From 354392ffcf1e662655d87c899cb91c673b4e3f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=88=98=ED=98=84?= Date: Thu, 18 Jun 2026 16:42:26 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[1=EB=8B=A8=EA=B3=84]=20JPA=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../common/GlobalExceptionHandler.java | 10 ++ .../roomescape/domain/reservation/Rank.java | 5 +- .../domain/reservation/Reservation.java | 76 ++++++--- .../domain/reservation/ReservationDate.java | 11 +- .../domain/reservation/ReservationName.java | 10 +- .../reservation/ReservationRepository.java | 23 +-- .../domain/reservation/ReservationTime.java | 23 ++- .../ReservationTimeRepository.java | 19 +-- .../roomescape/domain/reservation/Slot.java | 40 ++++- .../domain/reservation/SlotRepository.java | 24 ++- .../java/roomescape/domain/theme/Theme.java | 32 +++- .../roomescape/domain/theme/ThemeName.java | 11 +- .../domain/theme/ThemeRepository.java | 26 ++-- .../roomescape/domain/theme/ThumbnailUrl.java | 11 +- .../repository/JdbcReservationRepository.java | 146 ------------------ .../JdbcReservationTimeRepository.java | 80 ---------- .../repository/JdbcSlotRepository.java | 128 --------------- .../repository/JdbcThemeRepository.java | 97 ------------ .../service/ReservationService.java | 30 ++-- .../service/ReservationTimeService.java | 21 ++- .../java/roomescape/service/ThemeService.java | 7 +- src/main/resources/application.properties | 8 +- src/main/resources/schema.sql | 44 ------ .../repository/ReservationRepositoryTest.java | 67 +++----- .../repository/SlotRepositoryTest.java | 28 +--- .../service/ReservationServiceTest.java | 49 +++--- .../roomescape/service/ThemeServiceTest.java | 4 +- 28 files changed, 312 insertions(+), 719 deletions(-) delete mode 100644 src/main/java/roomescape/repository/JdbcReservationRepository.java delete mode 100644 src/main/java/roomescape/repository/JdbcReservationTimeRepository.java delete mode 100644 src/main/java/roomescape/repository/JdbcSlotRepository.java delete mode 100644 src/main/java/roomescape/repository/JdbcThemeRepository.java delete mode 100644 src/main/resources/schema.sql diff --git a/build.gradle b/build.gradle index 4d675e7b13..419e2f9784 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-validation' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/roomescape/common/GlobalExceptionHandler.java b/src/main/java/roomescape/common/GlobalExceptionHandler.java index b696c13167..3268212630 100644 --- a/src/main/java/roomescape/common/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/common/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -44,6 +45,15 @@ public ProblemDetail httpRequestMethodNotSupportedExceptionHandle(HttpRequestMet return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); } + @ExceptionHandler(InvalidDataAccessApiUsageException.class) + public ProblemDetail invalidDataAccessApiUsageExceptionHandle(InvalidDataAccessApiUsageException e) { + if (e.getCause() instanceof RoomEscapeException roomEscapeException) { + return roomEscapeExceptionHandle(roomEscapeException); + } + log.error("데이터베이스 관련 오류가 발생했습니다", e); + return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, DATABASE_ERROR); + } + @ExceptionHandler(DataIntegrityViolationException.class) public ProblemDetail dataIntegrityViolationExceptionHandle(DataIntegrityViolationException e) { log.info("데이터 무결성 위반 오류가 발생했습니다.", e); diff --git a/src/main/java/roomescape/domain/reservation/Rank.java b/src/main/java/roomescape/domain/reservation/Rank.java index db00d84fe7..49637dd3dd 100644 --- a/src/main/java/roomescape/domain/reservation/Rank.java +++ b/src/main/java/roomescape/domain/reservation/Rank.java @@ -1,7 +1,10 @@ package roomescape.domain.reservation; public class Rank { - private final long value; + private long value; + + protected Rank(){ + } public Rank(long value) { this.value = value; diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 176a41cdaf..53b17ea3ab 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -1,27 +1,59 @@ package roomescape.domain.reservation; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +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 jakarta.persistence.Transient; +import jakarta.persistence.UniqueConstraint; import roomescape.domain.DomainErrorCode; import roomescape.domain.RoomEscapeException; import java.time.LocalDateTime; +@Entity +@Table( + uniqueConstraints = @UniqueConstraint(name = "uq_reservation", + columnNames = {"slot_id", "name"} + )) public class Reservation { - private final Long id; - private final ReservationName name; - private final Status status; - private final Slot slot; - private final Rank rank; - public Reservation(Long id, ReservationName name, Status status, Slot slot) { - this(id, name, status, slot, null); + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private ReservationName name; + + @Enumerated(EnumType.STRING) + private Status status; + + @ManyToOne(optional = false) + @JoinColumn(name = "slot_id") + private Slot slot; + + @Transient + private Rank rank; + + @Column(name = "created_at", insertable = false, updatable = false, + columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") + private LocalDateTime createdAt; + + protected Reservation() { } - public Reservation(Long id, ReservationName name, Status status, Slot slot, Rank rank) { + public Reservation(Long id, ReservationName name, Status status, Slot slot) { this.id = id; this.name = name; this.status = status; this.slot = slot; - this.rank = rank; } public static Reservation load(Long id, String name, String status, Slot slot) { @@ -32,16 +64,22 @@ public static Reservation create(String name, Slot slot) { return new Reservation(null, new ReservationName(name), Status.WAITING, slot); } - public Reservation withId(Long id) { - return new Reservation(id, name, status, slot, rank); - } - public Reservation withStatus(Status status) { - return new Reservation(id, name, status, slot, rank); + return new Reservation(id, name, status, slot); } public Reservation withRank(Rank rank) { - return new Reservation(id, name, status, slot, rank); + Reservation copy = new Reservation(id, name, status, slot); + copy.rank = rank; + return copy; + } + + public void changeStatus(Status status) { + this.status = status; + } + + public void changeSlot(Slot slot) { + this.slot = slot; } public boolean isApproved() { @@ -78,10 +116,6 @@ public Status getStatus() { return status; } - public Rank getRank() { - return rank; - } - public Slot getSlot() { return slot; } @@ -90,6 +124,10 @@ public Long getSlotId() { return slot.getId(); } + public Rank getRank() { + return rank; + } + @Override public final boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/roomescape/domain/reservation/ReservationDate.java b/src/main/java/roomescape/domain/reservation/ReservationDate.java index 8a529034c4..4649b9e6ec 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationDate.java +++ b/src/main/java/roomescape/domain/reservation/ReservationDate.java @@ -1,13 +1,22 @@ package roomescape.domain.reservation; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + import java.time.LocalDate; import java.util.Objects; import static roomescape.domain.DomainErrorCode.INVALID_INPUT; import static roomescape.domain.DomainPreconditions.requireNonNull; +@Embeddable public class ReservationDate { - private final LocalDate date; + + @Column(nullable = false) + private LocalDate date; + + protected ReservationDate() { + } public ReservationDate(LocalDate date) { this.date = requireNonNull(date, INVALID_INPUT, "예약 날짜는 비어있을 수 없습니다."); diff --git a/src/main/java/roomescape/domain/reservation/ReservationName.java b/src/main/java/roomescape/domain/reservation/ReservationName.java index 572dda76be..ccf55dc42c 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationName.java +++ b/src/main/java/roomescape/domain/reservation/ReservationName.java @@ -1,15 +1,23 @@ package roomescape.domain.reservation; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + import java.util.Objects; import static roomescape.domain.DomainErrorCode.INVALID_INPUT; import static roomescape.domain.DomainPreconditions.require; import static roomescape.domain.DomainPreconditions.requireNonBlank; +@Embeddable public class ReservationName { private static final int MAX_NAME_LENGTH = 20; - private final String value; + @Column(name = "name", nullable = false) + private String value; + + protected ReservationName() { + } public ReservationName(String value) { requireNonBlank(value, INVALID_INPUT, "예약자 이름은 비어있을 수 없습니다."); diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index f14b4f3b75..e5fc8ababb 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -1,30 +1,17 @@ package roomescape.domain.reservation; +import org.springframework.data.jpa.repository.JpaRepository; import roomescape.domain.RoomEscapeException; +import java.util.List; import java.util.Optional; import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; +public interface ReservationRepository extends JpaRepository { + List findAllByName(ReservationName name); -public interface ReservationRepository { - Reservations findAll(); - - Optional findById(Long id); - - Reservations findByName(String name); - - Reservations findBySlotId(Long slotId); - - Reservation save(Reservation reservation); - - Reservation update(Long id, Reservation reservation); - - void updateStatusById(Long id, Status status); - - void deleteById(Long id); - - boolean existsById(Long id); + List findBySlot_Id(Long slotId); default Reservation getById(Long id) { return findById(id) diff --git a/src/main/java/roomescape/domain/reservation/ReservationTime.java b/src/main/java/roomescape/domain/reservation/ReservationTime.java index ac493e373c..7d30a55b27 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservation/ReservationTime.java @@ -1,13 +1,28 @@ package roomescape.domain.reservation; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + import java.time.LocalTime; import static roomescape.domain.DomainErrorCode.INVALID_INPUT; import static roomescape.domain.DomainPreconditions.requireNonNull; +@Entity 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; + + protected ReservationTime() { + } private ReservationTime(Long id, LocalTime startAt) { this.id = id; @@ -22,10 +37,6 @@ public static ReservationTime create(LocalTime startAt) { return new ReservationTime(null, startAt); } - public ReservationTime withId(Long id) { - return new ReservationTime(id, this.getStartAt()); - } - public Long getId() { return id; } diff --git a/src/main/java/roomescape/domain/reservation/ReservationTimeRepository.java b/src/main/java/roomescape/domain/reservation/ReservationTimeRepository.java index 6c624b18ab..97c5968ec7 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationTimeRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationTimeRepository.java @@ -1,27 +1,18 @@ package roomescape.domain.reservation; +import org.springframework.data.jpa.repository.JpaRepository; import roomescape.domain.RoomEscapeException; -import java.time.LocalDate; +import java.util.Collection; import java.util.List; -import java.util.Optional; import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; -public interface ReservationTimeRepository { - ReservationTime save(ReservationTime time); +public interface ReservationTimeRepository extends JpaRepository { - List findAll(); + List findByIdNotIn(Collection ids); - Optional findById(long id); - - List findByDateAndTheme(LocalDate date, long themeId); - - void delete(long id); - - boolean existsById(long id); - - default ReservationTime getById(long id) { + default ReservationTime getById(Long id) { return findById(id) .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 시간을 찾을 수 없습니다. : " + id)); } diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index e787ad9341..194d774590 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -1,5 +1,14 @@ package roomescape.domain.reservation; +import jakarta.persistence.Embedded; +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 roomescape.domain.DomainErrorCode; import roomescape.domain.RoomEscapeException; import roomescape.domain.theme.Theme; @@ -10,11 +19,30 @@ import static roomescape.domain.DomainErrorCode.INVALID_INPUT; import static roomescape.domain.DomainPreconditions.requireNonNull; +@Entity +@Table( + uniqueConstraints = @UniqueConstraint(name = "uq_slot", + columnNames = {"date", "time_id", "theme_id"} + )) public class Slot { - private final Long id; - private final ReservationDate date; - private final ReservationTime time; - private final Theme theme; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private ReservationDate date; + + @ManyToOne(optional = false) + @JoinColumn(name = "time_id") + private ReservationTime time; + + @ManyToOne(optional = false) + @JoinColumn(name = "theme_id") + private Theme theme; + + protected Slot() { + } public Slot(Long id, ReservationDate date, ReservationTime time, Theme theme) { this.id = id; @@ -39,10 +67,6 @@ public void validateNotPast(LocalDateTime now) { } } - public Slot withId(Long generatedKey) { - return new Slot(generatedKey, date, time, theme); - } - public boolean isPast(LocalDateTime now) { return LocalDateTime.of(date.getDate(), time.getStartAt()).isBefore(now); } diff --git a/src/main/java/roomescape/domain/reservation/SlotRepository.java b/src/main/java/roomescape/domain/reservation/SlotRepository.java index 261402e214..dacf38faf5 100644 --- a/src/main/java/roomescape/domain/reservation/SlotRepository.java +++ b/src/main/java/roomescape/domain/reservation/SlotRepository.java @@ -1,5 +1,8 @@ package roomescape.domain.reservation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import roomescape.domain.RoomEscapeException; import roomescape.domain.theme.Theme; @@ -9,26 +12,19 @@ import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; -public interface SlotRepository { - List findAll(); - - List findAllByName(String name); - - Optional findById(long slotId); - +public interface SlotRepository extends JpaRepository { Optional findByDateAndTimeAndTheme(ReservationDate date, ReservationTime time, Theme theme); - Slot save(Slot slot); - - Slot update(long id, Slot target); + List findByDateAndThemeId(ReservationDate date, Long themeId); - void deleteById(long id); + @Query("SELECT r.slot FROM Reservation r WHERE r.name.value = :name") + List findAllByName(@Param("name") String name); - boolean existsByTimeId(long timeId); + boolean existsByTimeId(Long timeId); - boolean existsByThemeId(long themeId); + boolean existsByThemeId(Long themeId); - default Slot getById(long id) { + default Slot getById(Long id) { return findById(id) .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 예약 슬롯을 찾을 수 없습니다. : " + id)); } diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index e316383a6e..f4574232c4 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -1,16 +1,36 @@ package roomescape.domain.theme; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + import java.util.Objects; import static roomescape.domain.DomainErrorCode.INVALID_INPUT; import static roomescape.domain.DomainPreconditions.requireNonBlank; import static roomescape.domain.DomainPreconditions.requireNonNull; +@Entity public class Theme { - private final Long id; - private final ThemeName name; - private final String description; - private final ThumbnailUrl thumbnailUrl; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private ThemeName name; + + @Column(nullable = false) + private String description; + + @Embedded + private ThumbnailUrl thumbnailUrl; + + protected Theme() { + } private Theme(Long id, ThemeName name, String description, ThumbnailUrl thumbnailUrl) { this.id = id; @@ -27,10 +47,6 @@ public static Theme create(ThemeName name, String description, ThumbnailUrl thum return new Theme(null, name, description, thumbnailUrl); } - public Theme withId(Long generatedKey) { - return new Theme(generatedKey, name, description, thumbnailUrl); - } - public Long getId() { return id; } diff --git a/src/main/java/roomescape/domain/theme/ThemeName.java b/src/main/java/roomescape/domain/theme/ThemeName.java index 9731bf673d..6dc5440b32 100644 --- a/src/main/java/roomescape/domain/theme/ThemeName.java +++ b/src/main/java/roomescape/domain/theme/ThemeName.java @@ -1,14 +1,23 @@ package roomescape.domain.theme; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + import java.util.Objects; import static roomescape.domain.DomainErrorCode.INVALID_INPUT; import static roomescape.domain.DomainPreconditions.require; import static roomescape.domain.DomainPreconditions.requireNonBlank; +@Embeddable public class ThemeName { private static final int MAX_NAME_LENGTH = 30; - private final String value; + + @Column(name = "name", nullable = false, length = MAX_NAME_LENGTH) + private String value; + + protected ThemeName() { + } public ThemeName(String value) { requireNonBlank(value, INVALID_INPUT, "테마 이름은 비어있을 수 없습니다."); diff --git a/src/main/java/roomescape/domain/theme/ThemeRepository.java b/src/main/java/roomescape/domain/theme/ThemeRepository.java index d8d4331dc6..ceb5fa2b30 100644 --- a/src/main/java/roomescape/domain/theme/ThemeRepository.java +++ b/src/main/java/roomescape/domain/theme/ThemeRepository.java @@ -1,27 +1,27 @@ package roomescape.domain.theme; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import roomescape.domain.RoomEscapeException; import java.time.LocalDate; import java.util.List; -import java.util.Optional; import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; -public interface ThemeRepository { - Theme save(Theme theme); +public interface ThemeRepository extends JpaRepository { - List findAll(); + @Query("SELECT t FROM Theme t " + + "JOIN Slot s ON s.theme = t " + + "JOIN Reservation r ON r.slot = s " + + "WHERE s.date.date > :fromDate AND s.date.date <= :date " + + "GROUP BY t " + + "ORDER BY COUNT(r) DESC") + List findFamous(@Param("fromDate") LocalDate fromDate, @Param("date") LocalDate date, Pageable pageable); - Optional findById(long themeId); - - List findFamous(long days, LocalDate date, long limit); - - void deleteById(long themeId); - - boolean existsById(long themeId); - - default Theme getById(long id) { + default Theme getById(Long id) { return findById(id) .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 테마를 찾을 수 없습니다. : " + id)); } diff --git a/src/main/java/roomescape/domain/theme/ThumbnailUrl.java b/src/main/java/roomescape/domain/theme/ThumbnailUrl.java index 5b9c5f3aa0..6951702b73 100644 --- a/src/main/java/roomescape/domain/theme/ThumbnailUrl.java +++ b/src/main/java/roomescape/domain/theme/ThumbnailUrl.java @@ -1,5 +1,8 @@ package roomescape.domain.theme; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + import java.util.Objects; import java.util.regex.Pattern; @@ -7,9 +10,15 @@ import static roomescape.domain.DomainPreconditions.require; import static roomescape.domain.DomainPreconditions.requireNonBlank; +@Embeddable public class ThumbnailUrl { private static final Pattern URL_PATTERN = Pattern.compile("^https?://.+"); - private final String value; + + @Column(name = "thumbnail_url", nullable = false) + private String value; + + protected ThumbnailUrl() { + } public ThumbnailUrl(String value) { requireNonBlank(value, INVALID_INPUT, "이미지 주소는 비어있을 수 없습니다."); diff --git a/src/main/java/roomescape/repository/JdbcReservationRepository.java b/src/main/java/roomescape/repository/JdbcReservationRepository.java deleted file mode 100644 index 7dadf9abe0..0000000000 --- a/src/main/java/roomescape/repository/JdbcReservationRepository.java +++ /dev/null @@ -1,146 +0,0 @@ -package roomescape.repository; - -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservation.ReservationRepository; -import roomescape.domain.reservation.ReservationTime; -import roomescape.domain.reservation.Reservations; -import roomescape.domain.reservation.Slot; -import roomescape.domain.reservation.Status; -import roomescape.domain.theme.Theme; - -import java.util.List; -import java.util.Optional; - -@Repository -public class JdbcReservationRepository implements ReservationRepository { - private static final String SELECT_BASE = """ - SELECT r.id AS reservation_id, - r.name AS reservation_name, - r.status AS reservation_status, - r.slot_id AS slot_id, - s.date AS slot_date, - t.id AS reservation_time_id, - t.start_at AS reservation_time_start_at, - th.id AS theme_id, - th.name AS theme_name, - th.description AS theme_description, - th.thumbnail_url AS theme_thumbnail_url - FROM reservation r - JOIN slot s ON r.slot_id = s.id - JOIN reservation_time t ON s.time_id = t.id - JOIN theme th ON s.theme_id = th.id - """; - - private static final RowMapper ROW_MAPPER = (rs, rowNum) -> { - ReservationTime reservationTime = ReservationTime.load( - rs.getLong("reservation_time_id"), - rs.getTime("reservation_time_start_at").toLocalTime() - ); - - Theme theme = Theme.load( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description"), - rs.getString("theme_thumbnail_url") - ); - - Slot slot = Slot.load( - rs.getLong("slot_id"), - rs.getDate("slot_date").toLocalDate(), - reservationTime, - theme - ); - - return Reservation.load( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_status"), - slot - ); - }; - - private final NamedParameterJdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert simpleJdbcInsert; - - public JdbcReservationRepository(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate.getJdbcTemplate()) - .withTableName("reservation") - .usingGeneratedKeyColumns("id") - .usingColumns("slot_id", "name", "status"); - } - - public Reservations findAll() { - return new Reservations(jdbcTemplate.query(SELECT_BASE, ROW_MAPPER)); - } - - public Optional findById(Long id) { - List result = jdbcTemplate.query( - SELECT_BASE + "WHERE r.id = :id", - new MapSqlParameterSource("id", id), - ROW_MAPPER); - return result.stream().findFirst(); - } - - public Reservations findByName(String name) { - MapSqlParameterSource param = new MapSqlParameterSource("name", name); - return new Reservations(jdbcTemplate.query( - SELECT_BASE + "WHERE r.name = :name", - param, - ROW_MAPPER)); - } - - public Reservations findBySlotId(Long slotId) { - MapSqlParameterSource param = new MapSqlParameterSource("slotId", slotId); - return new Reservations(jdbcTemplate.query( - SELECT_BASE + "WHERE r.slot_id = :slotId ORDER BY r.created_at ASC", - param, - ROW_MAPPER)); - } - - public Reservation update(Long id, Reservation reservation) { - MapSqlParameterSource params = new MapSqlParameterSource("id", id) - .addValue("slot_id", reservation.getSlotId()) - .addValue("name", reservation.getName().getValue()) - .addValue("status", reservation.getStatus().name()); - jdbcTemplate.update("UPDATE reservation SET slot_id = :slot_id, name = :name, status = :status WHERE id = :id", params); - return findById(id).orElseThrow(); - } - - public void updateStatusById(Long id, Status status) { - MapSqlParameterSource params = new MapSqlParameterSource("id", id) - .addValue("status", status.name()); - - jdbcTemplate.update("UPDATE reservation SET status = :status WHERE id = :id", params); - } - - public Reservation save(Reservation reservation) { - MapSqlParameterSource params = new MapSqlParameterSource("slot_id", reservation.getSlotId()) - .addValue("name", reservation.getName().getValue()) - .addValue("status", reservation.getStatus().name()); - - long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return reservation.withId(generatedKey); - } - - public void deleteById(Long id) { - MapSqlParameterSource param = new MapSqlParameterSource("id", id); - jdbcTemplate.update("DELETE FROM reservation WHERE id = :id", param); - } - - public boolean existsById(Long id) { - MapSqlParameterSource param = new MapSqlParameterSource("id", id); - return Boolean.TRUE.equals(jdbcTemplate.queryForObject(""" - SELECT EXISTS ( - SELECT 1 FROM reservation WHERE id = :id - ) - """, - param, - Boolean.class)); - } -} diff --git a/src/main/java/roomescape/repository/JdbcReservationTimeRepository.java b/src/main/java/roomescape/repository/JdbcReservationTimeRepository.java deleted file mode 100644 index ba2cccbe88..0000000000 --- a/src/main/java/roomescape/repository/JdbcReservationTimeRepository.java +++ /dev/null @@ -1,80 +0,0 @@ -package roomescape.repository; - -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservation.ReservationTime; -import roomescape.domain.reservation.ReservationTimeRepository; - -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; - -@Repository -public class JdbcReservationTimeRepository implements ReservationTimeRepository { - private static final RowMapper RESERVATION_TIME_ROW_MAPPER = (resultSet, rowNum) -> - ReservationTime.load(resultSet.getLong("id"), resultSet.getTime("start_at").toLocalTime()); - - private static final String BASE_SELECT = "select id, start_at from reservation_time"; - - private final NamedParameterJdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert simpleJdbcInsert; - - public JdbcReservationTimeRepository(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate.getJdbcTemplate()) - .withTableName("reservation_time") - .usingGeneratedKeyColumns("id"); - } - - public ReservationTime save(ReservationTime time) { - MapSqlParameterSource params = new MapSqlParameterSource() - .addValue("start_at", time.getStartAt()); - - long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return time.withId(generatedKey); - } - - public List findAll() { - return jdbcTemplate.query(BASE_SELECT, RESERVATION_TIME_ROW_MAPPER); - } - - public Optional findById(long id) { - MapSqlParameterSource param = new MapSqlParameterSource("id", id); - List result = jdbcTemplate.query(BASE_SELECT + " where id = :id", param, RESERVATION_TIME_ROW_MAPPER); - return result.stream().findFirst(); - } - - public List findByDateAndTheme(LocalDate date, long themeId) { - MapSqlParameterSource params = new MapSqlParameterSource() - .addValue("date", date) - .addValue("themeId", themeId); - - String sql = """ - SELECT rt.id, rt.start_at - FROM reservation_time AS rt - WHERE rt.id NOT IN ( - SELECT s.time_id - FROM slot s - INNER JOIN reservation r ON r.slot_id = s.id - WHERE s.date = :date AND s.theme_id = :themeId AND r.status = 'APPROVED' - ) - """; - return jdbcTemplate.query(sql, params, RESERVATION_TIME_ROW_MAPPER); - } - - public void delete(long id) { - MapSqlParameterSource param = new MapSqlParameterSource("id", id); - String sql = "delete from reservation_time where id = :id"; - - jdbcTemplate.update(sql, param); - } - - public boolean existsById(long reservationTimeId) { - MapSqlParameterSource param = new MapSqlParameterSource("id", reservationTimeId); - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject("SELECT EXISTS (SELECT 1 FROM reservation_time WHERE id = :id)", param, Boolean.class)); - } -} diff --git a/src/main/java/roomescape/repository/JdbcSlotRepository.java b/src/main/java/roomescape/repository/JdbcSlotRepository.java deleted file mode 100644 index ca99f443b2..0000000000 --- a/src/main/java/roomescape/repository/JdbcSlotRepository.java +++ /dev/null @@ -1,128 +0,0 @@ -package roomescape.repository; - -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservation.ReservationDate; -import roomescape.domain.reservation.ReservationTime; -import roomescape.domain.reservation.Slot; -import roomescape.domain.reservation.SlotRepository; -import roomescape.domain.theme.Theme; - -import java.util.List; -import java.util.Optional; - -@Repository -public class JdbcSlotRepository implements SlotRepository { - public static final RowMapper SLOT_ROW_MAPPER = (rs, rowNum) -> { - ReservationTime reservationTime = ReservationTime.load( - rs.getLong("reservation_time_id"), - rs.getTime("reservation_time_start_at").toLocalTime()); - Theme theme = Theme.load( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description"), - rs.getString("theme_thumbnail_url")); - return Slot.load( - rs.getLong("slot_id"), - rs.getDate("slot_date").toLocalDate(), - reservationTime, - theme); - }; - private static final String SELECT_BASE = """ - SELECT s.id AS slot_id, - s.date AS slot_date, - rt.id AS reservation_time_id, - rt.start_at AS reservation_time_start_at, - t.id AS theme_id, - t.name AS theme_name, - t.description AS theme_description, - t.thumbnail_url AS theme_thumbnail_url - FROM slot s - INNER JOIN reservation_time rt ON s.time_id = rt.id - INNER JOIN theme t ON s.theme_id = t.id - """; - private final NamedParameterJdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert simpleJdbcInsert; - - public JdbcSlotRepository(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate.getJdbcTemplate()) - .withTableName("slot") - .usingGeneratedKeyColumns("id") - .usingColumns("date", "time_id", "theme_id"); - } - - public List findAll() { - return jdbcTemplate.query(SELECT_BASE, SLOT_ROW_MAPPER); - } - - public List findAllByName(String name) { - return jdbcTemplate.query( - SELECT_BASE + "INNER JOIN reservation r ON r.slot_id = s.id WHERE r.name = :name", - new MapSqlParameterSource("name", name), - SLOT_ROW_MAPPER); - } - - public Optional findById(long slotId) { - List result = jdbcTemplate.query( - SELECT_BASE + "WHERE s.id = :slotId", - new MapSqlParameterSource("slotId", slotId), - SLOT_ROW_MAPPER); - return result.stream().findFirst(); - } - - public Optional findByDateAndTimeAndTheme(ReservationDate date, ReservationTime time, Theme theme) { - MapSqlParameterSource params = new MapSqlParameterSource() - .addValue("date", date.getDate()) - .addValue("timeId", time.getId()) - .addValue("themeId", theme.getId()); - - List result = jdbcTemplate.query( - SELECT_BASE + "WHERE s.date = :date AND s.time_id = :timeId AND s.theme_id = :themeId", - params, - SLOT_ROW_MAPPER); - return result.stream().findFirst(); - } - - public Slot save(Slot slot) { - MapSqlParameterSource params = new MapSqlParameterSource() - .addValue("date", slot.getDate().getDate()) - .addValue("time_id", slot.getTime().getId()) - .addValue("theme_id", slot.getTheme().getId()); - - long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return slot.withId(generatedKey); - } - - public Slot update(long id, Slot target) { - MapSqlParameterSource params = new MapSqlParameterSource() - .addValue("date", target.getDate().getDate()) - .addValue("time_id", target.getTime().getId()) - .addValue("theme_id", target.getTheme().getId()) - .addValue("id", id); - - jdbcTemplate.update("UPDATE slot SET date = :date, time_id = :time_id, theme_id = :theme_id WHERE id = :id", params); - return findById(id).orElseThrow(); - } - - public void deleteById(long id) { - jdbcTemplate.update("DELETE FROM slot WHERE id = :id", new MapSqlParameterSource("id", id)); - } - - public boolean existsByTimeId(long timeId) { - return Boolean.TRUE.equals(jdbcTemplate.queryForObject( - "SELECT EXISTS (SELECT 1 FROM slot WHERE time_id = :timeId)", - new MapSqlParameterSource("timeId", timeId), - Boolean.class)); - } - - public boolean existsByThemeId(long themeId) { - return Boolean.TRUE.equals(jdbcTemplate.queryForObject( - "SELECT EXISTS (SELECT 1 FROM slot WHERE theme_id = :themeId)", - new MapSqlParameterSource("themeId", themeId), - Boolean.class)); - } -} diff --git a/src/main/java/roomescape/repository/JdbcThemeRepository.java b/src/main/java/roomescape/repository/JdbcThemeRepository.java deleted file mode 100644 index e153190745..0000000000 --- a/src/main/java/roomescape/repository/JdbcThemeRepository.java +++ /dev/null @@ -1,97 +0,0 @@ -package roomescape.repository; - -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; -import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeRepository; - -import java.time.LocalDate; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -@Repository -public class JdbcThemeRepository implements ThemeRepository { - public static final RowMapper THEME_ROW_MAPPER = (rs, rowNum) -> - Theme.load(rs.getLong("id"), - rs.getString("name"), - rs.getString("description"), - rs.getString("thumbnail_url")); - - private static final String BASE_SQL = "SELECT id, name, description, thumbnail_url FROM THEME"; - - private final NamedParameterJdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert simpleJdbcInsert; - - public JdbcThemeRepository(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate.getJdbcTemplate()) - .withTableName("theme") - .usingGeneratedKeyColumns("id"); - } - - public Theme save(Theme theme) { - Map params = Map.of( - "name", theme.getName().getValue(), - "description", theme.getDescription(), - "thumbnail_url", theme.getThumbnailUrl().getValue() - ); - - long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return theme.withId(generatedKey); - } - - public List findAll() { - return jdbcTemplate.query(BASE_SQL, THEME_ROW_MAPPER); - } - - public List findFamous(long days, LocalDate date, long limit) { - LocalDate startDate = date.minusDays(days); - LocalDate endDate = date.minusDays(1); - - String sql = """ - SELECT t.id, t.name, t.description, t.thumbnail_url - FROM THEME AS t - INNER JOIN ( - SELECT theme_id, count(theme_id) AS cnt - FROM RESERVATION - WHERE date BETWEEN :startDate AND :endDate - GROUP BY theme_id - ORDER BY count(theme_id) DESC, theme_id DESC - LIMIT :limit - ) AS topN ON t.id = topN.theme_id - ORDER BY topN.cnt DESC, topN.theme_id DESC - """; - - MapSqlParameterSource params = new MapSqlParameterSource() - .addValue("startDate", startDate) - .addValue("endDate", endDate) - .addValue("limit", limit); - - return jdbcTemplate.query(sql, params, THEME_ROW_MAPPER); - } - - public void deleteById(long themeId) { - MapSqlParameterSource param = new MapSqlParameterSource("id", themeId); - jdbcTemplate.update("DELETE FROM theme WHERE id = :id", param); - } - - public boolean existsById(long themeId) { - MapSqlParameterSource param = new MapSqlParameterSource("id", themeId); - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject( - "SELECT EXISTS (SELECT 1 FROM theme WHERE id = :id)", - param, - Boolean.class) - ); - } - - public Optional findById(long themeId) { - MapSqlParameterSource param = new MapSqlParameterSource("id", themeId); - List result = jdbcTemplate.query(BASE_SQL + " WHERE id = :id", param, THEME_ROW_MAPPER); - return result.stream().findFirst(); - } -} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index a123321b1f..b36b6bbda8 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -3,6 +3,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; @@ -33,37 +34,42 @@ public Reservation reserve(ReservationCreateCommand command) { Reservation assembled = assembler.from(command); Slot slot = assembled.getSlot(); - Reservations existing = reservationRepository.findBySlotId(slot.getId()); + Reservations existing = new Reservations(reservationRepository.findBySlot_Id(slot.getId())); Reservation join = existing.join(assembled); return reservationRepository.save(join); } public Reservation find(long id) { Reservation reservation = reservationRepository.getById(id); - Reservations slotReservations = reservationRepository.findBySlotId(reservation.getSlotId()); + Reservations slotReservations = new Reservations(reservationRepository.findBySlot_Id(reservation.getSlotId())); return reservation.withRank(slotReservations.rankOf(reservation)); } public Reservations findAll(String name) { if (name == null) { - return reservationRepository.findAll(); + return new Reservations(reservationRepository.findAll()); } - return reservationRepository.findByName(name); + return new Reservations(reservationRepository.findAllByName(new ReservationName(name))); } @Transactional public Reservation update(ReservationUpdateCommand command, long id) { Reservation existing = reservationRepository.getById(id); Reservation assembled = assembler.from(command); + Slot newSlot = assembled.getSlot(); + Long oldSlotId = existing.getSlotId(); + + Reservations slotReservations = new Reservations(reservationRepository.findBySlot_Id(newSlot.getId())).excluding(id); + Reservation template = slotReservations.join(assembled); - Reservations slotReservations = reservationRepository.findBySlotId(newSlot.getId()).excluding(id); - Reservation updated = slotReservations.join(assembled); - reservationRepository.update(id, updated); + boolean wasApproved = existing.isApproved(); + existing.changeSlot(template.getSlot()); + existing.changeStatus(template.getStatus()); - boolean slotChanged = !existing.getSlotId().equals(newSlot.getId()); - if (slotChanged && existing.isApproved()) { - promoteFirstWaiting(existing.getSlotId()); + boolean slotChanged = !oldSlotId.equals(newSlot.getId()); + if (slotChanged && wasApproved) { + promoteFirstWaiting(oldSlotId); } return find(id); @@ -85,8 +91,8 @@ public void cancel(long reservationId, String name) { } private void promoteFirstWaiting(Long slotId) { - reservationRepository.findBySlotId(slotId) + new Reservations(reservationRepository.findBySlot_Id(slotId)) .firstWaiting() - .ifPresent(waiting -> reservationRepository.updateStatusById(waiting.getId(), Status.APPROVED)); + .ifPresent(waiting -> waiting.changeStatus(Status.APPROVED)); } } diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java index ef01063038..fd2e2ff032 100644 --- a/src/main/java/roomescape/service/ReservationTimeService.java +++ b/src/main/java/roomescape/service/ReservationTimeService.java @@ -6,6 +6,7 @@ import roomescape.controller.dto.request.ReservationTimeCreateRequest; import roomescape.domain.DomainErrorCode; import roomescape.domain.RoomEscapeException; +import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.ReservationTimeRepository; import roomescape.domain.reservation.SlotRepository; @@ -13,6 +14,8 @@ import java.time.Clock; import java.time.LocalDate; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Service @Transactional(readOnly = true) @@ -33,8 +36,7 @@ public ReservationTimeService( @Transactional public ReservationTime create(ReservationTimeCreateRequest request) { - ReservationTime reservationTime = ReservationTime.create(request.getStartAt()); - return reservationTimeRepository.save(reservationTime); + return reservationTimeRepository.save(ReservationTime.create(request.getStartAt())); } public List findAll() { @@ -48,11 +50,20 @@ public List findAvailable(AvailableTimeFindRequest request) { throw new RoomEscapeException(DomainErrorCode.PAST_DATE, "지나간 날짜는 조회할 수 없습니다: " + request.getDate()); } - return reservationTimeRepository.findByDateAndTheme(request.getDate(), request.getThemeId()); + Set bookedTimeIds = slotRepository + .findByDateAndThemeId(new ReservationDate(request.getDate()), request.getThemeId()) + .stream() + .map(slot -> slot.getTime().getId()) + .collect(Collectors.toSet()); + + if (bookedTimeIds.isEmpty()) { + return reservationTimeRepository.findAll(); + } + return reservationTimeRepository.findByIdNotIn(bookedTimeIds); } @Transactional - public void delete(long reservationTimeId) { + public void delete(Long reservationTimeId) { if (!reservationTimeRepository.existsById(reservationTimeId)) { throw new RoomEscapeException(DomainErrorCode.RESOURCE_NOT_FOUND, "해당 예약 시간을 찾을 수 없습니다: " + reservationTimeId); } @@ -61,6 +72,6 @@ public void delete(long reservationTimeId) { throw new RoomEscapeException(DomainErrorCode.RESOURCE_IN_USE, "해당 예약 시간은 사용 중이라 삭제할 수 없습니다: " + reservationTimeId); } - reservationTimeRepository.delete(reservationTimeId); + reservationTimeRepository.deleteById(reservationTimeId); } } diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java index eca8eac01e..78cd00c16e 100644 --- a/src/main/java/roomescape/service/ThemeService.java +++ b/src/main/java/roomescape/service/ThemeService.java @@ -1,5 +1,6 @@ package roomescape.service; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.controller.dto.request.ThemeCreateRequest; @@ -42,7 +43,7 @@ public Theme create(ThemeCreateRequest request) { return themeRepository.save(theme); } - public Theme find(long themeId) { + public Theme find(Long themeId) { return themeRepository.getById(themeId); } @@ -51,7 +52,9 @@ public List findAll() { } public List findFamous(int limit, int days, LocalDate date) { - return themeRepository.findFamous(days, date, limit); + LocalDate to = date != null ? date : LocalDate.now(); + LocalDate fromDate = to.minusDays(days); + return themeRepository.findFamous(fromDate, to, PageRequest.of(0, limit)); } @Transactional diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f597afeef4..d848dce0c8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,9 @@ spring.h2.console.enabled=true spring.h2.console.path=/h2-console -spring.datasource.url=jdbc:h2:mem:database \ No newline at end of file +spring.datasource.url=jdbc:h2:mem:database + + +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.defer-datasource-initialization=true \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index 9a3c712eaf..0000000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,44 +0,0 @@ -DROP TABLE IF EXISTS reservation; -DROP TABLE IF EXISTS slot; -DROP TABLE IF EXISTS reservation_time; -DROP TABLE IF EXISTS theme; - -CREATE TABLE theme -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(20) NOT NULL, - description VARCHAR(255) NOT NULL, - thumbnail_url VARCHAR(255) NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE reservation_time -( - id BIGINT NOT NULL AUTO_INCREMENT, - start_at TIME NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE slot -( - id BIGINT NOT NULL AUTO_INCREMENT, - date DATE NOT NULL, - time_id BIGINT NOT NULL, - theme_id BIGINT NOT NULL, - PRIMARY KEY (id), - FOREIGN KEY (time_id) REFERENCES reservation_time (id), - FOREIGN KEY (theme_id) REFERENCES theme (id), - CONSTRAINT uq_slot UNIQUE (date, time_id, theme_id) -); - -CREATE TABLE reservation -( - id BIGINT NOT NULL AUTO_INCREMENT, - slot_id BIGINT NOT NULL, - name VARCHAR(20) NOT NULL, - status VARCHAR(10) NOT NULL DEFAULT 'APPROVED', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - FOREIGN KEY (slot_id) REFERENCES slot (id), - CONSTRAINT uq_reservation UNIQUE (slot_id, name) -); diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index 99ef9b6474..e1fe0c4d27 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -4,15 +4,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; -import org.springframework.context.annotation.Import; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.dao.DataAccessException; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; +import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.ReservationTimeRepository; -import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.SlotRepository; import roomescape.domain.reservation.Status; @@ -27,6 +26,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; +import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -34,13 +34,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; -@JdbcTest -@Import(value = { - JdbcReservationTimeRepository.class, - JdbcThemeRepository.class, - JdbcSlotRepository.class, - JdbcReservationRepository.class -}) +@DataJpaTest class ReservationRepositoryTest { private final static Clock FIXED_CLOCK = Clock.fixed( Instant.parse("2026-05-10T03:00:00Z"), @@ -101,7 +95,7 @@ void save_throwsException_whenSameSlotAndName() { assertThatThrownBy(() -> { Reservation conflict = Reservation.create("유저", slot).withStatus(Status.WAITING); - reservationRepository.save(conflict); + reservationRepository.saveAndFlush(conflict); }).isInstanceOf(DataAccessException.class); } @@ -116,9 +110,9 @@ void findAll() { reservationRepository.save(given1); reservationRepository.save(given2); - Reservations all = reservationRepository.findAll(); + List all = reservationRepository.findAll(); - assertThat(all.getValues().size()).isEqualTo(2); + assertThat(all).hasSize(2); } @Test @@ -156,7 +150,7 @@ void deleteById() { @Test @DisplayName("slot_id의 모든 예약 조회") - void findBySlotId() { + void findBySlot_Id() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); @@ -165,9 +159,9 @@ void findBySlotId() { reservationRepository.save(given1); reservationRepository.save(given2); - Reservations bySlotId = reservationRepository.findBySlotId(slot.getId()); + List bySlotId = reservationRepository.findBySlot_Id(slot.getId()); - assertThat(bySlotId.getValues().size()).isEqualTo(2); + assertThat(bySlotId).hasSize(2); } @Test @@ -179,8 +173,8 @@ void existsBySlotIdAndName() { Reservation given = Reservation.create("유저", slot).withStatus(Status.APPROVED); Reservation saved = reservationRepository.save(given); - Reservations reservations = reservationRepository.findBySlotId(slot.getId()); - boolean exists = reservations.hasByName(saved); + List reservations = reservationRepository.findBySlot_Id(slot.getId()); + boolean exists = reservations.stream().anyMatch(r -> r.isSameName(saved)); assertThat(exists).isTrue(); } @@ -194,29 +188,27 @@ void existsApprovedBySlotId() { Reservation given = Reservation.create("유저", slot).withStatus(Status.APPROVED); reservationRepository.save(given); - Reservations reservations = reservationRepository.findBySlotId(slot.getId()); - boolean hasApproved = reservations.getValues().stream().anyMatch(Reservation::isApproved); + List reservations = reservationRepository.findBySlot_Id(slot.getId()); + boolean hasApproved = reservations.stream().anyMatch(Reservation::isApproved); assertThat(hasApproved).isTrue(); } @Test - @DisplayName("id에 따른 status 업데이트 후 조회") - void updateStatusById() { + @DisplayName("id에 따른 status 더티 체킹 후 조회") + void changeStatus() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); Reservation given = Reservation.create("유저", slot).withStatus(Status.WAITING); Reservation saved = reservationRepository.save(given); - reservationRepository.updateStatusById(saved.getId(), Status.APPROVED); + reservationRepository.getById(saved.getId()).changeStatus(Status.APPROVED); Optional found = reservationRepository.findById(saved.getId()); assertSoftly(softly -> { softly.assertThat(found).isPresent(); - softly.assertThat(found.get().getId()).isEqualTo(saved.getId()); - softly.assertThat(found.get().getName()).isEqualTo(saved.getName()); softly.assertThat(found.get().getStatus()).isEqualTo(Status.APPROVED); }); } @@ -232,29 +224,10 @@ void findByName() { reservationRepository.save(given1); reservationRepository.save(given2); - Reservations byName = reservationRepository.findByName("유저1"); - - assertThat(byName.getValues()).hasSize(1); - assertThat(byName.getValues().get(0).getName().getValue()).isEqualTo("유저1"); - } - - @Test - @DisplayName("예약 수정 후 조회") - void update() { - ReservationTime time = givenTime(14); - Theme theme = givenTheme("테스트 테마"); - Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given = Reservation.create("유저", slot).withStatus(Status.WAITING); - Reservation saved = reservationRepository.save(given); - - Reservation updated = Reservation.create("수정유저", slot).withStatus(Status.APPROVED); - Reservation result = reservationRepository.update(saved.getId(), updated); + List byName = reservationRepository.findAllByName(new ReservationName("유저1")); - assertSoftly(softly -> { - softly.assertThat(result.getId()).isEqualTo(saved.getId()); - softly.assertThat(result.getName().getValue()).isEqualTo("수정유저"); - softly.assertThat(result.getStatus()).isEqualTo(Status.APPROVED); - }); + assertThat(byName).hasSize(1); + assertThat(byName.get(0).getName().getValue()).isEqualTo("유저1"); } @Test diff --git a/src/test/java/roomescape/repository/SlotRepositoryTest.java b/src/test/java/roomescape/repository/SlotRepositoryTest.java index 8134e7ab28..c9a1373c04 100644 --- a/src/test/java/roomescape/repository/SlotRepositoryTest.java +++ b/src/test/java/roomescape/repository/SlotRepositoryTest.java @@ -3,8 +3,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; -import org.springframework.context.annotation.Import; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationRepository; @@ -30,13 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; -@JdbcTest -@Import(value = { - JdbcReservationTimeRepository.class, - JdbcThemeRepository.class, - JdbcSlotRepository.class, - JdbcReservationRepository.class -}) +@DataJpaTest class SlotRepositoryTest { private final static Clock FIXED_CLOCK = Clock.fixed( Instant.parse("2026-05-10T03:00:00Z"), @@ -154,23 +147,6 @@ void deleteById() { assertThat(slotRepository.findById(saved.getId())).isEmpty(); } - @Test - @DisplayName("수정 후 조회") - void update() { - ReservationTime time = givenTime(14); - ReservationTime newTime = givenTime(16); - Theme theme = givenTheme("테스트 테마"); - Slot saved = givenSlot(new ReservationDate(TODAY), time, theme); - - Slot target = Slot.load(saved.getId(), TODAY, newTime, theme); - Slot updated = slotRepository.update(saved.getId(), target); - - assertSoftly(softly -> { - softly.assertThat(updated.getId()).isEqualTo(saved.getId()); - softly.assertThat(updated.getTime().getStartAt()).isEqualTo(LocalTime.of(16, 0)); - }); - } - @Test @DisplayName("해당 timeId를 사용하는 슬롯 존재 확인") void existsByTimeId_true() { diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index 2675675a86..c0ad6e6cdd 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -12,6 +12,7 @@ import roomescape.domain.DomainErrorCode; import roomescape.domain.RoomEscapeException; import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Reservations; @@ -26,9 +27,9 @@ import java.time.ZoneOffset; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -45,7 +46,6 @@ class ReservationServiceTest { Theme.load(1L, "any", "any", URL) ); private static final Reservation DUMMY = Reservation.load(1L, NAME, "APPROVED", DUMMY_SLOT); - private static final Reservations DUMMIES = new Reservations(List.of(DUMMY)); private static final long NOT_EXISTS_ID = Long.MAX_VALUE; private static final long EXISTS_ID = 1L; @@ -68,7 +68,7 @@ private void givenNow(LocalDateTime dateTime) { void 예약_취소_성공() { givenNow(LocalDateTime.of(2026, 1, 1, 0, 0)); given(reservationRepository.getById(1L)).willReturn(DUMMY); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of())); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of()); reservationService.cancel(1L, NAME); verify(reservationRepository).deleteById(1L); } @@ -100,7 +100,7 @@ private void givenNow(LocalDateTime dateTime) { void 미래로_예약하면_성공해야_한다() { Reservation assembled = Reservation.create(NAME, DUMMY_SLOT); given(assembler.from(any(ReservationCreateCommand.class))).willReturn(assembled); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of())); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of()); given(reservationRepository.save(any())).willReturn(DUMMY); Assertions.assertThatNoException().isThrownBy(() -> reservationService.reserve( ReservationCreateCommand.from(new ReservationCreateRequest("zeze", LocalDate.parse("2026-04-05"), 1L, 1L)))); @@ -111,7 +111,7 @@ private void givenNow(LocalDateTime dateTime) { Reservation assembled = Reservation.create("zeze", DUMMY_SLOT); given(assembler.from(any(ReservationCreateCommand.class))).willReturn(assembled); Reservation existingReservation = Reservation.load(1L, "zeze", "APPROVED", DUMMY_SLOT); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of(existingReservation))); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(existingReservation)); Assertions.assertThatThrownBy(() -> reservationService.reserve( ReservationCreateCommand.from(new ReservationCreateRequest("zeze", LocalDate.parse("2099-04-05"), 1L, 1L)))) @@ -155,7 +155,7 @@ private void givenNow(LocalDateTime dateTime) { given(reservationRepository.getById(1L)).willReturn(DUMMY); given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create("zeze", newSlot)); Reservation conflicting = Reservation.load(2L, "zeze", "APPROVED", newSlot); - given(reservationRepository.findBySlotId(2L)).willReturn(new Reservations(List.of(conflicting))); + given(reservationRepository.findBySlot_Id(2L)).willReturn(List.of(conflicting)); Assertions.assertThatThrownBy(() -> reservationService.update(ReservationUpdateCommand.from(request), 1L)) .isInstanceOf(RoomEscapeException.class); @@ -176,10 +176,9 @@ private void givenNow(LocalDateTime dateTime) { given(reservationRepository.getById(1L)).willReturn(existing); given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(name, slot)); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of(existing))); - given(reservationRepository.update(eq(1L), any())).willReturn(existing); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(existing)); - Assertions.assertThatCode(() -> reservationService.update(ReservationUpdateCommand.from(request), 1L)) + assertThatCode(() -> reservationService.update(ReservationUpdateCommand.from(request), 1L)) .doesNotThrowAnyException(); } @@ -204,7 +203,7 @@ private void givenNow(LocalDateTime dateTime) { givenNow(LocalDateTime.of(2026, 1, 1, 0, 0)); Reservation reservation = RoomEscapeFixture.reservation(); given(reservationRepository.getById(EXISTS_ID)).willReturn(reservation); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of())); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of()); assertThatCode(() -> reservationService.cancel(EXISTS_ID, reservation.getName().getValue())) .doesNotThrowAnyException(); } @@ -216,11 +215,11 @@ private void givenNow(LocalDateTime dateTime) { Reservation waiting = Reservation.load(2L, "대기자", "WAITING", DUMMY_SLOT); given(reservationRepository.getById(1L)).willReturn(approved); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of(waiting))); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(waiting)); reservationService.cancel(1L, NAME); - verify(reservationRepository).updateStatusById(2L, Status.APPROVED); + assertThat(waiting.getStatus()).isEqualTo(Status.APPROVED); } @Test @@ -231,7 +230,7 @@ private void givenNow(LocalDateTime dateTime) { reservationService.cancel(1L, NAME); - verify(reservationRepository, never()).updateStatusById(any(), any()); + verify(reservationRepository, never()).findBySlot_Id(any()); } @Test @@ -242,12 +241,12 @@ private void givenNow(LocalDateTime dateTime) { given(reservationRepository.getById(1L)).willReturn(existing); given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(NAME, newSlot)); - given(reservationRepository.findBySlotId(2L)).willReturn(new Reservations(List.of())); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of(waitingInOldSlot))); + given(reservationRepository.findBySlot_Id(2L)).willReturn(List.of()); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(waitingInOldSlot)); reservationService.update(ReservationUpdateCommand.from(new ReservationUpdateRequest(NAME, LocalDate.of(2099, 6, 1), 1L, 1L)), 1L); - verify(reservationRepository).updateStatusById(3L, Status.APPROVED); + assertThat(waitingInOldSlot.getStatus()).isEqualTo(Status.APPROVED); } @Test @@ -256,17 +255,17 @@ private void givenNow(LocalDateTime dateTime) { given(reservationRepository.getById(1L)).willReturn(existing); given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(NAME, DUMMY_SLOT)); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of(existing))); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(existing)); - reservationService.update(ReservationUpdateCommand.from(new ReservationUpdateRequest(NAME, LocalDate.of(2099, 1, 1), 1L, 1L)), 1L); - - verify(reservationRepository, never()).updateStatusById(any(), any()); + assertThatCode(() -> reservationService.update( + ReservationUpdateCommand.from(new ReservationUpdateRequest(NAME, LocalDate.of(2099, 1, 1), 1L, 1L)), 1L)) + .doesNotThrowAnyException(); } @Test void 단건_조회시_존재하는_ID면_결과를_반환한다() { given(reservationRepository.getById(EXISTS_ID)).willReturn(DUMMY); - given(reservationRepository.findBySlotId(EXISTS_ID)).willReturn(DUMMIES); + given(reservationRepository.findBySlot_Id(EXISTS_ID)).willReturn(List.of(DUMMY)); Reservation result = reservationService.find(EXISTS_ID); Assertions.assertThat(result.getId()).isEqualTo(EXISTS_ID); @@ -281,7 +280,7 @@ private void givenNow(LocalDateTime dateTime) { @Test void 이름_없이_목록_조회시_전체_예약을_반환한다() { - given(reservationRepository.findAll()).willReturn(new Reservations(List.of(DUMMY))); + given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); Reservations results = reservationService.findAll(null); @@ -291,7 +290,7 @@ private void givenNow(LocalDateTime dateTime) { @Test void 이름으로_목록_조회시_해당_이름의_예약만_반환한다() { - given(reservationRepository.findByName(NAME)).willReturn(new Reservations(List.of(DUMMY))); + given(reservationRepository.findAllByName(new ReservationName(NAME))).willReturn(List.of(DUMMY)); Reservations results = reservationService.findAll(NAME); @@ -302,7 +301,7 @@ private void givenNow(LocalDateTime dateTime) { @Test void 첫번째_예약은_승인_상태이다() { given(reservationRepository.getById(EXISTS_ID)).willReturn(DUMMY); - given(reservationRepository.findBySlotId(EXISTS_ID)).willReturn(DUMMIES); + given(reservationRepository.findBySlot_Id(EXISTS_ID)).willReturn(List.of(DUMMY)); Reservation result = reservationService.find(EXISTS_ID); Assertions.assertThat(result.getStatus()).isEqualTo(Status.APPROVED); @@ -319,7 +318,7 @@ private void givenNow(LocalDateTime dateTime) { Reservation approved = Reservation.load(1L, NAME, "APPROVED", waitingSlot); Reservation waiting = Reservation.load(2L, "대기자", "WAITING", waitingSlot); given(reservationRepository.getById(2L)).willReturn(waiting); - given(reservationRepository.findBySlotId(1L)).willReturn(new Reservations(List.of(approved, waiting))); + given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(approved, waiting)); Reservation result = reservationService.find(2L); diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java index bc488e0f5c..49f4dc06eb 100644 --- a/src/test/java/roomescape/service/ThemeServiceTest.java +++ b/src/test/java/roomescape/service/ThemeServiceTest.java @@ -11,6 +11,8 @@ import roomescape.domain.reservation.SlotRepository; import roomescape.domain.theme.ThemeRepository; +import org.springframework.data.domain.PageRequest; + import java.time.LocalDate; import static org.mockito.BDDMockito.given; @@ -57,7 +59,7 @@ public class ThemeServiceTest { themeService.findFamous(limit, days, date); // then - verify(themeRepository).findFamous(days, date, limit); + verify(themeRepository).findFamous(date.minusDays(days), date, PageRequest.of(0, limit)); } @Test From 30094e0a0a59f64d8d75412be84090b66977c0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=88=98=ED=98=84?= Date: Fri, 19 Jun 2026 03:31:57 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[1=EB=8B=A8=EA=B3=84]=20=EC=8B=A4=ED=97=98?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/controller/TestController.java | 40 +++++ .../roomescape/controller/TestService.java | 36 ++++ .../domain/reservation/Reservation.java | 1 + .../roomescape/domain/reservation/Slot.java | 15 ++ .../java/roomescape/domain/theme/Theme.java | 4 + src/main/resources/application.properties | 3 +- .../FetchDefaultObservationTest.java | 131 ++++++++++++++ .../PersistenceContextObservationTest.java | 166 ++++++++++++++++++ .../roomescape/SlotReservationSyncTest.java | 99 +++++++++++ .../roomescape/WriteBehindComparisonTest.java | 107 +++++++++++ 10 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 src/main/java/roomescape/controller/TestController.java create mode 100644 src/main/java/roomescape/controller/TestService.java create mode 100644 src/test/java/roomescape/FetchDefaultObservationTest.java create mode 100644 src/test/java/roomescape/PersistenceContextObservationTest.java create mode 100644 src/test/java/roomescape/SlotReservationSyncTest.java create mode 100644 src/test/java/roomescape/WriteBehindComparisonTest.java diff --git a/src/main/java/roomescape/controller/TestController.java b/src/main/java/roomescape/controller/TestController.java new file mode 100644 index 0000000000..7f60083f8f --- /dev/null +++ b/src/main/java/roomescape/controller/TestController.java @@ -0,0 +1,40 @@ +package roomescape.controller; + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import roomescape.domain.reservation.Slot; +import roomescape.domain.reservation.SlotRepository; + +import java.time.LocalTime; + + +/*** + * 양방향 매핑 테스트 컨트롤러 + * + * 1단계 이후 삭제할 예정 + */ +@RestController +public class TestController { + + private final TestService service; + + public TestController(TestService service) { + this.service = service; + } + + @GetMapping("/test1") + public ResponseEntity getTest1() { + return ResponseEntity.ok(service.test1()); + } + + @GetMapping("/test2") + public ResponseEntity getTest2() { + return ResponseEntity.ok(service.test2()); + } +} diff --git a/src/main/java/roomescape/controller/TestService.java b/src/main/java/roomescape/controller/TestService.java new file mode 100644 index 0000000000..19f1153bdc --- /dev/null +++ b/src/main/java/roomescape/controller/TestService.java @@ -0,0 +1,36 @@ +package roomescape.controller; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.Slot; +import roomescape.domain.reservation.SlotRepository; + +import java.time.LocalTime; +import java.util.List; + + +/*** + * 양방향 매핑 테스트 서비스 + * + * 1단계 이후 삭제할 예정 + */ + +@Service +@Transactional(readOnly = true) +public class TestService { + + private final SlotRepository repository; + + public TestService(SlotRepository repository) { + this.repository = repository; + } + + public Slot test1() { + return repository.getById(1L); + } + + public LocalTime test2() { + return repository.getById(1L).getTime().getStartAt(); + } +} diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 53b17ea3ab..942ba57c03 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -5,6 +5,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index 194d774590..83356d63ca 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -7,6 +7,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import roomescape.domain.DomainErrorCode; @@ -15,6 +16,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import static roomescape.domain.DomainErrorCode.INVALID_INPUT; import static roomescape.domain.DomainPreconditions.requireNonNull; @@ -41,6 +44,14 @@ public class Slot { @JoinColumn(name = "theme_id") private Theme theme; + /** + * 미션 1 이후 양방향 삭제 고려 + * + * - 테스트를 위한 양방향 + */ + @OneToMany(mappedBy = "slot") + private List reservations = new ArrayList<>(); + protected Slot() { } @@ -75,6 +86,10 @@ public Long getId() { return id; } + public List getReservations() { + return reservations; + } + public ReservationDate getDate() { return date; } diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index f4574232c4..53cdedbeb7 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -76,4 +76,8 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hashCode(id); } + + public void changeDescription(String description) { + this.description = description; + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d848dce0c8..68e44f68c9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,4 +6,5 @@ spring.datasource.url=jdbc:h2:mem:database spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.hibernate.ddl-auto=create-drop -spring.jpa.defer-datasource-initialization=true \ No newline at end of file +spring.jpa.defer-datasource-initialization=true +spring.jpa.open-in-view=false \ No newline at end of file diff --git a/src/test/java/roomescape/FetchDefaultObservationTest.java b/src/test/java/roomescape/FetchDefaultObservationTest.java new file mode 100644 index 0000000000..61a7bd4b43 --- /dev/null +++ b/src/test/java/roomescape/FetchDefaultObservationTest.java @@ -0,0 +1,131 @@ +package roomescape; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.ArrayList; +import java.util.List; + +@DataJpaTest +public class FetchDefaultObservationTest { + + @Autowired + private EntityManager em; + + // ---- 관찰 전용 엔티티: fetch를 "명시하지 않음" ---- + + @Entity(name = "FetchParent") + static class FetchParent { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + String name; + + @OneToMany(mappedBy = "parent") // ★ fetch 미명시 → 기본 LAZY + List children = new ArrayList<>(); + + FetchParent() {} + FetchParent(String name) { this.name = name; } + } + + @Entity(name = "FetchChild") + static class FetchChild { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + @ManyToOne // ★ fetch 미명시 → 기본 EAGER + FetchParent parent; + + FetchChild() {} + FetchChild(FetchParent parent) { this.parent = parent; } + } + + // ================================================================ + // 관찰 A. @ManyToOne 기본값 = EAGER + // Child를 조회하면 parent(@ManyToOne)가 같이 딸려 나온다. + // ================================================================ + @Test + @DisplayName("@ManyToOne 기본 EAGER: Child 조회 시 parent까지 함께 조회됨") + void manyToOne_isEagerByDefault() { + FetchParent parent = new FetchParent("부모"); + em.persist(parent); + FetchChild child = new FetchChild(parent); + em.persist(child); + em.flush(); + em.clear(); // 1차 캐시 비우기 (중요: 안 비우면 캐시에서 반환돼 SELECT 안 나감) + + System.out.println(">>> [ManyToOne] JPQL로 Child 조회 시작"); + FetchChild found = em.createQuery( + "select c from FetchChild c where c.id = :id", FetchChild.class) + .setParameter("id", child.id) + .getSingleResult(); + System.out.println(">>> [ManyToOne] 조회 끝 — 위에 parent SELECT까지 나갔는지 확인"); + System.out.println(">>> [ManyToOne] 이미 parent가 로딩됨 (EAGER): " + found.parent.name); + + // 예측: @ManyToOne 기본 EAGER → Child 조회 시 parent도 함께 SELECT + // (JPQL이라 보통 별도 SELECT로 parent를 추가 조회 = N+1 형태로 드러남) + } + + // ================================================================ + // 관찰 B. @OneToMany 기본값 = LAZY + // Parent를 조회해도 children은 안 나온다. + // children에 "실제로 접근하는 순간"에야 SELECT가 따로 나간다. + // ================================================================ + @Test + @DisplayName("@OneToMany 기본 LAZY: Parent 조회 시 children은 미조회, 접근 시 조회") + void oneToMany_isLazyByDefault() { + FetchParent parent = new FetchParent("부모"); + em.persist(parent); + em.persist(new FetchChild(parent)); + em.persist(new FetchChild(parent)); + em.flush(); + em.clear(); + + System.out.println(">>> [OneToMany] JPQL로 Parent 조회 시작"); + FetchParent found = em.createQuery( + "select p from FetchParent p where p.id = :id", FetchParent.class) + .setParameter("id", parent.id) + .getSingleResult(); + System.out.println(">>> [OneToMany] Parent 조회 끝 — children SELECT는 아직 안 나갔을 것 (LAZY)"); + + System.out.println(">>> [OneToMany] 이제 children에 접근 — 이 순간 SELECT가 따로 나갈 것"); + int count = found.children.size(); // ★ 여기서 LAZY 초기화 트리거 + System.out.println(">>> [OneToMany] children 접근 후 — 위에 children SELECT가 찍혔는지 확인. size=" + count); + + // 예측: Parent 조회 시점엔 children SELECT 없음 + // found.children.size() 호출하는 순간 children SELECT 발행 + } + + // ================================================================ + // 관찰 C. 대조 — em.find는 LAZY여도 JOIN으로 끌어올 수 있다 + // (왜 위에서 JPQL을 썼는지 보여주는 대조 실험) + // ================================================================ + @Test + @DisplayName("대조: em.find는 ManyToOne을 JOIN으로 한 번에 가져옴") + void emFind_usesJoin() { + FetchParent parent = new FetchParent("부모"); + em.persist(parent); + FetchChild child = new FetchChild(parent); + em.persist(child); + em.flush(); + em.clear(); + + System.out.println(">>> [em.find] Child를 em.find로 조회 — parent까지 JOIN 한 방으로 나올 것"); + FetchChild found = em.find(FetchChild.class, child.id); + System.out.println(">>> [em.find] 위 SELECT가 join을 포함한 단일 쿼리였는지 확인"); + + // 같은 EAGER인데 JPQL(관찰 A)은 별도 SELECT, em.find는 JOIN 한 방. + // → "EAGER = JOIN"이 아니라 "조회 방식이 SQL 전략을 결정한다" + } +} diff --git a/src/test/java/roomescape/PersistenceContextObservationTest.java b/src/test/java/roomescape/PersistenceContextObservationTest.java new file mode 100644 index 0000000000..a8d3575b02 --- /dev/null +++ b/src/test/java/roomescape/PersistenceContextObservationTest.java @@ -0,0 +1,166 @@ +package roomescape; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationDate; +import roomescape.domain.reservation.ReservationTime; +import roomescape.domain.reservation.Slot; +import roomescape.domain.theme.Theme; +import roomescape.domain.theme.ThemeName; +import roomescape.domain.theme.ThemeRepository; +import roomescape.domain.theme.ThumbnailUrl; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 영속성 컨텍스트 관찰 테스트 + * + * 목적: 코드를 추가하기보다 JPA가 자동으로 무엇을 하는지 "관찰"한다. + * + * 관찰 방법: + * - 콘솔의 Hibernate SQL 로그를 직접 눈으로 본다 (show-sql=true 필요) + * - em.flush() : 영속성 컨텍스트 → DB 동기화를 강제로 트리거 + * - em.clear() : 1차 캐시를 비워 다음 조회가 DB를 다시 치게 함 + * + * 각 테스트의 핵심은 assert 통과가 아니라 + * "이 시점에 SQL이 나가는가/안 나가는가"를 로그로 확인하는 것이다. + */ + +@DataJpaTest +public class PersistenceContextObservationTest { + private final static Clock FIXED_CLOCK = Clock.fixed( + Instant.parse("2026-05-10T03:00:00Z"), + ZoneId.of("Asia/Seoul") + ); + + private final static LocalDate TODAY = LocalDate.of(2026, 5, 10); + + @Autowired private ThemeRepository themeRepository; + @Autowired private EntityManager em; + + + /** + * 관찰 1. Dirty Checking (변경 감지) + * - @Transactional 안에서 엔티티 필드만 수정, save() 미호출 + * - commit/flush 시점에 UPDATE가 자동 발행되는지 본다 + */ + @Test + @DisplayName("변경 감지: save 안 불러도 flush 시 UPDATE 발행") + void dirtyChecking() { + Theme theme = Theme.create(new ThemeName("테스트 테마"), "테스트 테마 입니다.", new ThumbnailUrl("https://test.com")); + + em.persist(theme); + em.flush(); + em.clear(); // 1차 캐시 비우고 깨끗하게 다시 조회 + + // when: 조회한 영속 엔티티의 필드만 수정 (save 호출 X) + Theme found = em.find(Theme.class, theme.getId()); + found.changeDescription("수정된 설명"); + + System.out.println(">>> 아직 flush 전 — UPDATE 안 나갔을 것"); + em.flush(); // ★ 이 순간 로그에 UPDATE가 찍히는지 관찰 + System.out.println(">>> flush 후 — 위에 UPDATE 로그 떴는지 확인"); + + // 예측: save를 안 불렀는데도 flush 시점에 UPDATE 발행 + // 실제: 로그로 확인 + } + + /** + * 관찰 2. 1차 캐시 + * - 같은 트랜잭션에서 findById 두 번 + * - 두 번째 SELECT가 생략되는지 본다 + */ + @Test + @DisplayName("1차 캐시: 같은 트랜잭션 내 두 번째 조회는 SELECT 생략") + void firstLevelCache() { + Theme theme = Theme.create(new ThemeName("테스트 테마"), "테스트 테마 입니다.", new ThumbnailUrl("https://test.com")); + + em.persist(theme); + em.flush(); + em.clear(); // 1차 캐시 비우고 깨끗하게 다시 조회 + + System.out.println(">>> 첫 번째 find — SELECT 나갈 것"); + Theme first = em.find(Theme.class, theme.getId()); + + System.out.println(">>> 두 번째 find — SELECT 안 나갈 것 (1차 캐시)"); + Theme second = em.find(Theme.class, theme.getId()); + + // 예측: 두 번째 find는 DB를 안 친다 (캐시 적중) + // 실제: 로그에 SELECT가 한 번만 찍히는지 확인 + assertThat(first).isSameAs(second); // 같은 인스턴스 = 캐시에서 반환 + } + + /** + * 관찰 3. 쓰기 지연 (write-behind) + * - persist 호출 후 flush 전/후 비교 + * - INSERT가 persist 시점이 아니라 flush 시점에 나가는지 본다 + */ + @Test + @DisplayName("쓰기 지연: INSERT는 persist가 아니라 flush 시점에 발행") + void writeBehind() { + testEntity testEntity = new testEntity(); + + System.out.println(">>> persist 호출 — INSERT 아직 안 나갈 것"); + em.persist(testEntity); + System.out.println(">>> persist 후 — 위에 INSERT 로그 없어야 정상"); + + System.out.println(">>> flush 호출 직전"); + em.flush(); // ★ 이 순간 INSERT가 찍히는지 관찰 + System.out.println(">>> flush 후 — 위에 INSERT 로그 떴는지 확인"); + + // 예측: INSERT가 persist가 아니라 flush 시점에 발행됨 + // (단, IDENTITY 전략이면 persist 시점에 바로 INSERT 나갈 수 있음 — 관찰 포인트!) + } + + @Entity + @SequenceGenerator(name = "seq_gen", sequenceName = "my_seq", allocationSize = 1) + static class testEntity { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_gen") + private Long id; + + protected testEntity(){ + } + } + + /*** + * 관찰 4. flush 시점 — JPQL 실행 직전 자동 flush + * - 수정 후 JPQL을 실행하면, 그 직전에 flush가 자동 트리거되는지 본다 + */ + @Test + @DisplayName("flush 시점: JPQL 실행 직전 자동 flush") + void flushBeforeQuery() { + Theme theme = Theme.create(new ThemeName("테스트 테마"), "테스트 테마 입니다.", new ThumbnailUrl("https://test.com")); + + em.persist(theme); + em.flush(); + em.clear(); // 1차 캐시 비우고 깨끗하게 다시 조회 + + Theme found = em.find(Theme.class, theme.getId()); + found.changeDescription("변경"); // 수정만, flush 안 함 + + System.out.println(">>> JPQL 실행 — 그 직전에 UPDATE가 자동으로 나갈 것"); + em.createQuery("select t from Theme t", Theme.class).getResultList(); + System.out.println(">>> 위에 UPDATE가 SELECT보다 먼저 찍혔는지 확인"); + + // 예측: JPQL 실행 전 자동 flush → UPDATE가 SELECT보다 먼저 + // 이유: JPQL은 DB를 직접 조회하므로, 변경분을 먼저 반영해야 일관성 유지 + } +} diff --git a/src/test/java/roomescape/SlotReservationSyncTest.java b/src/test/java/roomescape/SlotReservationSyncTest.java new file mode 100644 index 0000000000..82a6443f8b --- /dev/null +++ b/src/test/java/roomescape/SlotReservationSyncTest.java @@ -0,0 +1,99 @@ +package roomescape; + +import jakarta.persistence.EntityManager; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.dao.DataAccessException; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationDate; +import roomescape.domain.reservation.ReservationRepository; +import roomescape.domain.reservation.ReservationTime; +import roomescape.domain.reservation.ReservationTimeRepository; +import roomescape.domain.reservation.Slot; +import roomescape.domain.reservation.SlotRepository; +import roomescape.domain.theme.Theme; +import roomescape.domain.theme.ThemeName; +import roomescape.domain.theme.ThemeRepository; +import roomescape.domain.theme.ThumbnailUrl; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +/*** + * 양방향 FK 수정 위치 테스트 + * + * 1단계 이후 삭제할 예정 + */ +@DataJpaTest +public class SlotReservationSyncTest { + private final static Clock FIXED_CLOCK = Clock.fixed( + Instant.parse("2026-05-10T03:00:00Z"), + ZoneId.of("Asia/Seoul") + ); + + private final static LocalDate TODAY = LocalDate.of(2026, 5, 10); + + @Autowired private SlotRepository slotRepository; + @Autowired private ReservationRepository reservationRepository; + @Autowired private ReservationTimeRepository timeRepository; + @Autowired private ThemeRepository themeRepository; + @Autowired private EntityManager em; + + private ReservationTime givenTime(int hour) { + return timeRepository.save(ReservationTime.create(LocalTime.of(hour, 0))); + } + + private Theme givenTheme(String name) { + return themeRepository.save(Theme.create(new ThemeName(name), "테스트 테마 입니다.", new ThumbnailUrl("https://test.com"))); + } + + private Slot givenSlot(ReservationDate date, ReservationTime time, Theme theme) { + return slotRepository.save(Slot.create(date, time, theme, LocalDateTime.now(FIXED_CLOCK))); + } + + @Test + void 거울만_수정하면_FK가_반영되지_않는다() { + ReservationTime time = givenTime(14); + Theme theme = givenTheme("테스트 테마"); + + Slot slot = slotRepository.save(Slot.create(new ReservationDate(TODAY), time, theme, LocalDateTime.now(FIXED_CLOCK))); + Reservation reservation = Reservation.create("김철수", slot); + + slot.getReservations().add(reservation); + + em.flush(); // DB에 강제로 반영 시도 + em.clear(); // 영속성 컨텍스트 비우고 DB에서 다시 읽게 + + List found = reservationRepository.findAll(); + assertThat(found).isEmpty(); + } + + @Test + void 주인을_수정해야_FK가_반영된다() { + ReservationTime time = givenTime(14); + Theme theme = givenTheme("테스트 테마"); + + Slot slot = slotRepository.save(Slot.create(new ReservationDate(TODAY), time, theme, LocalDateTime.now(FIXED_CLOCK))); + Reservation reservation = Reservation.create("김철수", slot); + + reservationRepository.save(reservation); + + em.flush(); + em.clear(); + + List found = reservationRepository.findAll(); + assertThat(found).hasSize(1); + assertThat(found.getFirst().getSlotId()).isEqualTo(slot.getId()); + } +} diff --git a/src/test/java/roomescape/WriteBehindComparisonTest.java b/src/test/java/roomescape/WriteBehindComparisonTest.java new file mode 100644 index 0000000000..f465cf78b9 --- /dev/null +++ b/src/test/java/roomescape/WriteBehindComparisonTest.java @@ -0,0 +1,107 @@ +package roomescape; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +/** + * 쓰기 지연(write-behind) 비교 — IDENTITY vs SEQUENCE + * + * 관찰 목표: + * 같은 persist인데 id 생성 전략에 따라 INSERT 시점이 다르다. + * - IDENTITY : persist 시점에 즉시 INSERT (쓰기 지연 X) + * - SEQUENCE : persist는 id만 받고, INSERT는 flush까지 지연 (쓰기 지연 O) + * + * 관찰 방법: + * System.out 마킹과 Hibernate SQL 로그의 순서를 대조한다. + * "persist 후" 마킹과 "flush 후" 마킹 사이에 INSERT 로그가 어디 찍히는지 본다. + * + * Deep Dive: + * - SEQUENCE가 뭔지 -> 전체적인 ID 생성법 + */ +@DataJpaTest +public class WriteBehindComparisonTest { + + @Autowired + private EntityManager em; + + // ---- 전략 1: IDENTITY (DB auto_increment) ---- + @Entity(name = "IdentityEntity") + static class IdentityEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + } + + // ---- 전략 2: SEQUENCE (DB 시퀀스) ---- + @Entity(name = "SequenceEntity") + @SequenceGenerator(name = "seq_gen", sequenceName = "my_seq", allocationSize = 1) + static class SequenceEntity { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_gen") + Long id; + } + + // IDENTITY — persist 시점에 즉시 INSERT + @Test + @DisplayName("IDENTITY: persist 시점에 바로 INSERT (지연 안 됨)") + void identity_insertsImmediately() { + IdentityEntity e = new IdentityEntity(); + + System.out.println(">>>>> [IDENTITY] persist 호출 직전"); + em.persist(e); + System.out.println(">>>>> [IDENTITY] persist 후 — 여기 위에 INSERT 로그가 이미 떴을 것!"); + System.out.println(">>>>> [IDENTITY] persist 직후 id = " + e.id + " (벌써 채워짐)"); + + System.out.println(">>>>> [IDENTITY] flush 호출 직전"); + em.flush(); + System.out.println(">>>>> [IDENTITY] flush 후 — 여기엔 INSERT 새로 안 뜸 (이미 나갔으니)"); + + // 예측: INSERT가 "persist 후" 마킹 위에 찍힌다 (즉시 발행) + // 이유: IDENTITY는 INSERT해야 id가 생겨서, 캐시에 넣으려면 즉시 INSERT 필요 + } + + // SEQUENCE — persist는 id만, INSERT는 flush까지 지연 + @Test + @DisplayName("SEQUENCE: persist는 id만 받고 INSERT는 flush 시점") + void sequence_defersInsert() { + SequenceEntity e = new SequenceEntity(); + + System.out.println(">>>>> [SEQUENCE] persist 호출 직전"); + em.persist(e); + System.out.println(">>>>> [SEQUENCE] persist 후 — 여기 위엔 시퀀스 조회(call next value)만 있고 INSERT는 아직 없을 것!"); + System.out.println(">>>>> [SEQUENCE] persist 직후 id = " + e.id + " (시퀀스에서 받아 채워짐, INSERT는 아직)"); + + System.out.println(">>>>> [SEQUENCE] flush 호출 직전"); + em.flush(); + System.out.println(">>>>> [SEQUENCE] flush 후 — 여기 위에 INSERT가 이제야 찍혔을 것!"); + + // 예측: "persist 후"엔 시퀀스 조회만, INSERT는 "flush 후" 위에 찍힘 (지연됨) + // 이유: SEQUENCE는 INSERT 없이 id를 미리 받을 수 있어, INSERT를 flush까지 미룸 + } + + /** + * 결정적 비교 — 여러 개 persist 후 한 번에 flush + * - SEQUENCE는 INSERT 3개가 flush 때 몰려서 나가고, IDENTITY는 persist마다 INSERT가 흩어져 나간다. + */ + @Test + @DisplayName("다건: SEQUENCE는 flush 때 INSERT 몰림, IDENTITY는 persist마다 흩어짐") + void batchComparison() { + System.out.println(">>>>> [SEQUENCE 다건] persist 3번 시작"); + em.persist(new SequenceEntity()); + em.persist(new SequenceEntity()); + em.persist(new SequenceEntity()); + System.out.println(">>>>> [SEQUENCE 다건] persist 3번 끝 — INSERT 아직 안 나갔을 것 (시퀀스 조회만)"); + + System.out.println(">>>>> [SEQUENCE 다건] flush — 여기서 INSERT 3개가 몰려서 나갈 것"); + em.flush(); + System.out.println(">>>>> [SEQUENCE 다건] flush 끝"); + } +} From 042e6c62dcf4d5458dac7820caf019051d9076a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=88=98=ED=98=84?= Date: Sat, 20 Jun 2026 03:34:32 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[2=EB=8B=A8=EA=B3=84]=20=EB=82=B4=20?= =?UTF-8?q?=EC=98=88=EC=95=BD=20=EB=AA=A9=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReservationController.java | 14 +- .../dto/request/ReservationCreateRequest.java | 13 +- .../dto/request/ReservationUpdateRequest.java | 13 +- .../dto/response/ReservationResponse.java | 2 +- .../java/roomescape/domain/member/Member.java | 38 +++ .../domain/member/MemberRepository.java | 14 + .../domain/reservation/Reservation.java | 42 +-- .../reservation/ReservationRepository.java | 16 +- .../domain/reservation/Reservations.java | 12 +- .../roomescape/domain/reservation/Slot.java | 5 +- .../domain/reservation/SlotRepository.java | 4 +- .../service/ReservationAssembler.java | 14 +- .../service/ReservationCreateCommand.java | 13 +- .../service/ReservationService.java | 30 ++- .../service/ReservationUpdateCommand.java | 12 +- src/main/resources/data.sql | 253 +++++++----------- .../java/roomescape/MissionStep2Test.java | 9 +- .../java/roomescape/MissionStep3Test.java | 5 +- src/test/java/roomescape/MissionStepTest.java | 7 +- .../java/roomescape/RoomEscapeFixture.java | 10 +- .../roomescape/RoomescapeApplicationTest.java | 81 +++--- .../roomescape/SlotReservationSyncTest.java | 9 +- .../roomescape/WriteBehindComparisonTest.java | 2 +- .../controller/ReservationControllerTest.java | 39 +-- .../domain/reservation/ReservationTest.java | 8 +- .../repository/ReservationRepositoryTest.java | 80 +++--- .../repository/SlotRepositoryTest.java | 21 +- .../service/ReservationServiceTest.java | 111 ++++---- 28 files changed, 472 insertions(+), 405 deletions(-) create mode 100644 src/main/java/roomescape/domain/member/Member.java create mode 100644 src/main/java/roomescape/domain/member/MemberRepository.java diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java index 43256d0f32..a50ab706d6 100644 --- a/src/main/java/roomescape/controller/ReservationController.java +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -46,9 +46,9 @@ public ResponseEntity create( @GetMapping("/reservations") public ResponseEntity findList( - @RequestParam(required = false) String name + @RequestParam(required = false) Long memberId ) { - Reservations reservations = reservationService.findAll(name); + Reservations reservations = reservationService.findAll(memberId); return ResponseEntity.ok(ReservationResponses.toDto(reservations)); } @@ -58,12 +58,18 @@ public ResponseEntity find(@PathVariable long id) { return ResponseEntity.ok(ReservationResponse.toDto(reservation)); } + @GetMapping("/reservations-mine") + public ResponseEntity findMine(@RequestParam Long memberId) { + Reservations reservations = reservationService.findMine(memberId); + return ResponseEntity.ok(ReservationResponses.toDto(reservations)); + } + @DeleteMapping("/reservations/{id}") public ResponseEntity delete( @PathVariable long id, - @RequestParam String name + @RequestParam Long memberId ) { - reservationService.cancel(id, name); + reservationService.cancel(id, memberId); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/roomescape/controller/dto/request/ReservationCreateRequest.java b/src/main/java/roomescape/controller/dto/request/ReservationCreateRequest.java index dfcbbc8d36..7cbdf1abd0 100644 --- a/src/main/java/roomescape/controller/dto/request/ReservationCreateRequest.java +++ b/src/main/java/roomescape/controller/dto/request/ReservationCreateRequest.java @@ -6,8 +6,9 @@ import java.time.LocalDate; public class ReservationCreateRequest { - @NotNull(message = "이름은 필수로 입력해야 합니다") - private final String name; + @NotNull(message = "회원 ID는 필수로 입력해야 합니다.") + @Positive(message = "회원 ID는 양수여야 합니다.") + private final Long memberId; @NotNull(message = "날짜는 필수로 입력해야 합니다") private final LocalDate date; @@ -20,15 +21,15 @@ public class ReservationCreateRequest { @Positive(message = "Theme ID는 양수여야 합니다.") private final Long themeId; - public ReservationCreateRequest(String name, LocalDate date, Long timeId, Long themeId) { - this.name = name; + public ReservationCreateRequest(Long memberId, LocalDate date, Long timeId, Long themeId) { + this.memberId = memberId; this.date = date; this.timeId = timeId; this.themeId = themeId; } - public String getName() { - return name; + public Long getMemberId() { + return memberId; } public LocalDate getDate() { diff --git a/src/main/java/roomescape/controller/dto/request/ReservationUpdateRequest.java b/src/main/java/roomescape/controller/dto/request/ReservationUpdateRequest.java index 55cdbb8508..d5bc7f42cf 100644 --- a/src/main/java/roomescape/controller/dto/request/ReservationUpdateRequest.java +++ b/src/main/java/roomescape/controller/dto/request/ReservationUpdateRequest.java @@ -6,8 +6,9 @@ import java.time.LocalDate; public class ReservationUpdateRequest { - @NotNull(message = "이름은 필수로 입력해야 합니다") - private final String name; + @NotNull(message = "회원 ID는 필수로 입력해야 합니다.") + @Positive(message = "회원 ID는 양수여야 합니다.") + private final Long memberId; @NotNull(message = "날짜는 필수로 입력해야 합니다") private final LocalDate date; @@ -20,15 +21,15 @@ public class ReservationUpdateRequest { @Positive(message = "Theme ID는 양수여야 합니다.") private final Long themeId; - public ReservationUpdateRequest(String name, LocalDate date, Long timeId, Long themeId) { - this.name = name; + public ReservationUpdateRequest(Long memberId, LocalDate date, Long timeId, Long themeId) { + this.memberId = memberId; this.date = date; this.timeId = timeId; this.themeId = themeId; } - public String getName() { - return name; + public Long getMemberId() { + return memberId; } public LocalDate getDate() { diff --git a/src/main/java/roomescape/controller/dto/response/ReservationResponse.java b/src/main/java/roomescape/controller/dto/response/ReservationResponse.java index b461047f36..5b188cb891 100644 --- a/src/main/java/roomescape/controller/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/controller/dto/response/ReservationResponse.java @@ -30,7 +30,7 @@ public static ReservationResponse toDto(Reservation reservation) { Long rank = reservation.getRank() != null ? reservation.getRank().getValue() : null; return new ReservationResponse( reservation.getId(), - reservation.getName().getValue(), + reservation.getMember().getName(), slot.getDate().getDate(), reservation.getStatus().getKoreanName(), rank, diff --git a/src/main/java/roomescape/domain/member/Member.java b/src/main/java/roomescape/domain/member/Member.java new file mode 100644 index 0000000000..77056a99d0 --- /dev/null +++ b/src/main/java/roomescape/domain/member/Member.java @@ -0,0 +1,38 @@ +package roomescape.domain.member; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + protected Member() { + } + + public Member(Long id, String name) { + this.id = id; + this.name = name; + } + + public static Member create(String name) { + return new Member(null, name); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/roomescape/domain/member/MemberRepository.java b/src/main/java/roomescape/domain/member/MemberRepository.java new file mode 100644 index 0000000000..1e63fcdba7 --- /dev/null +++ b/src/main/java/roomescape/domain/member/MemberRepository.java @@ -0,0 +1,14 @@ +package roomescape.domain.member; + +import org.springframework.data.jpa.repository.JpaRepository; +import roomescape.domain.RoomEscapeException; + +import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; + +public interface MemberRepository extends JpaRepository { + + default Member getById(Long id) { + return findById(id) + .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 회원을 찾을 수 없습니다. : " + id)); + } +} diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 942ba57c03..87d799c1c3 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -1,7 +1,6 @@ package roomescape.domain.reservation; import jakarta.persistence.Column; -import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -16,13 +15,14 @@ import jakarta.persistence.UniqueConstraint; import roomescape.domain.DomainErrorCode; import roomescape.domain.RoomEscapeException; +import roomescape.domain.member.Member; import java.time.LocalDateTime; @Entity @Table( uniqueConstraints = @UniqueConstraint(name = "uq_reservation", - columnNames = {"slot_id", "name"} + columnNames = {"slot_id", "member_id"} )) public class Reservation { @@ -30,13 +30,14 @@ public class Reservation { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Embedded - private ReservationName name; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "member_id") + private Member member; @Enumerated(EnumType.STRING) private Status status; - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "slot_id") private Slot slot; @@ -50,27 +51,26 @@ public class Reservation { protected Reservation() { } - public Reservation(Long id, ReservationName name, Status status, Slot slot) { + public Reservation(Long id, Member member, Status status, Slot slot) { this.id = id; - this.name = name; + this.member = member; this.status = status; this.slot = slot; } - public static Reservation load(Long id, String name, String status, Slot slot) { - return new Reservation(id, new ReservationName(name), Status.from(status), slot); - } - - public static Reservation create(String name, Slot slot) { - return new Reservation(null, new ReservationName(name), Status.WAITING, slot); + public static Reservation create(Member member, Slot slot) { + if (member == null) { + throw new RoomEscapeException(DomainErrorCode.INVALID_INPUT, "회원은 null일 수 없습니다."); + } + return new Reservation(null, member, Status.WAITING, slot); } public Reservation withStatus(Status status) { - return new Reservation(id, name, status, slot); + return new Reservation(id, member, status, slot); } public Reservation withRank(Rank rank) { - Reservation copy = new Reservation(id, name, status, slot); + Reservation copy = new Reservation(id, member, status, slot); copy.rank = rank; return copy; } @@ -91,16 +91,16 @@ public boolean isWaiting() { return status == Status.WAITING; } - public boolean isSameName(Reservation other) { - return name.isSame(other.name); + public boolean isSameMember(Reservation other) { + return member.getId().equals(other.member.getId()); } public void validateCancellable(LocalDateTime now) { slot.validateNotPast(now); } - public void validateOwner(String ownerName) { - if (!name.isSame(new ReservationName(ownerName))) { + public void validateOwner(Long memberId) { + if (!member.getId().equals(memberId)) { throw new RoomEscapeException(DomainErrorCode.FORBIDDEN, "본인의 예약만 취소할 수 있습니다."); } } @@ -109,8 +109,8 @@ public Long getId() { return id; } - public ReservationName getName() { - return name; + public Member getMember() { + return member; } public Status getStatus() { diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index e5fc8ababb..a6780faedd 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -1,18 +1,30 @@ package roomescape.domain.reservation; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import roomescape.domain.RoomEscapeException; +import roomescape.domain.member.Member; import java.util.List; -import java.util.Optional; import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; public interface ReservationRepository extends JpaRepository { - List findAllByName(ReservationName name); + List findAllByMember(Member member); List findBySlot_Id(Long slotId); + List findByMemberId(Long memberId); + + @Query("select r from Reservation r " + + "join fetch r.member " + + "join fetch r.slot s " + + "join fetch s.theme " + + "join fetch s.time " + + "where r.member.id = :memberId") + List findMineWithDetails(@Param("memberId") Long memberId); + default Reservation getById(Long id) { return findById(id) .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 예약을 찾을 수 없습니다. : " + id)); diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java index 04990c987e..5fe6079ba9 100644 --- a/src/main/java/roomescape/domain/reservation/Reservations.java +++ b/src/main/java/roomescape/domain/reservation/Reservations.java @@ -14,7 +14,7 @@ public Reservations(List values) { } public Reservation join(Reservation assembled) { - conflictByName(assembled); + conflictByMember(assembled); Reservation withStatus = assembled.withStatus(nextStatus()); return withStatus.withRank(rankOf(withStatus)); } @@ -23,15 +23,15 @@ private Status nextStatus() { return values.stream().anyMatch(Reservation::isApproved) ? Status.WAITING : Status.APPROVED; } - public void conflictByName(Reservation reservation) { - if (hasByName(reservation)) { - throw new RoomEscapeException(DomainErrorCode.ALREADY_EXISTS, "이미 같은 슬롯에 예약이 존재합니다: " + reservation.getName().getValue()); + public void conflictByMember(Reservation reservation) { + if (hasByMember(reservation)) { + throw new RoomEscapeException(DomainErrorCode.ALREADY_EXISTS, "이미 같은 슬롯에 예약이 존재합니다: " + reservation.getMember().getName()); } } - public boolean hasByName(Reservation other) { + public boolean hasByMember(Reservation other) { return values.stream() - .anyMatch(r -> r.isSameName(other)); + .anyMatch(r -> r.isSameMember(other)); } public Reservations excluding(Long id) { diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index 83356d63ca..c1a1e99e69 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -2,6 +2,7 @@ import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -36,11 +37,11 @@ public class Slot { @Embedded private ReservationDate date; - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "time_id") private ReservationTime time; - @ManyToOne(optional = false) + @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "theme_id") private Theme theme; diff --git a/src/main/java/roomescape/domain/reservation/SlotRepository.java b/src/main/java/roomescape/domain/reservation/SlotRepository.java index dacf38faf5..491331bc05 100644 --- a/src/main/java/roomescape/domain/reservation/SlotRepository.java +++ b/src/main/java/roomescape/domain/reservation/SlotRepository.java @@ -17,8 +17,8 @@ public interface SlotRepository extends JpaRepository { List findByDateAndThemeId(ReservationDate date, Long themeId); - @Query("SELECT r.slot FROM Reservation r WHERE r.name.value = :name") - List findAllByName(@Param("name") String name); + @Query("SELECT r.slot FROM Reservation r WHERE r.member.id = :memberId") + List findAllByMemberId(@Param("memberId") Long memberId); boolean existsByTimeId(Long timeId); diff --git a/src/main/java/roomescape/service/ReservationAssembler.java b/src/main/java/roomescape/service/ReservationAssembler.java index 970f124ff8..31ec13bbec 100644 --- a/src/main/java/roomescape/service/ReservationAssembler.java +++ b/src/main/java/roomescape/service/ReservationAssembler.java @@ -1,6 +1,8 @@ package roomescape.service; import org.springframework.stereotype.Component; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationTime; @@ -18,35 +20,39 @@ public class ReservationAssembler { private final Clock clock; + private final MemberRepository members; private final ReservationTimeRepository reservationTimes; private final ThemeRepository themes; private final SlotRepository slots; public ReservationAssembler( Clock clock, + MemberRepository members, ReservationTimeRepository reservationTimes, ThemeRepository themes, SlotRepository slots ) { this.clock = clock; + this.members = members; this.reservationTimes = reservationTimes; this.themes = themes; this.slots = slots; } public Reservation from(ReservationCreateCommand command) { - return of(command.getName(), command.getDate(), command.getTimeId(), command.getThemeId()); + return of(command.getMemberId(), command.getDate(), command.getTimeId(), command.getThemeId()); } public Reservation from(ReservationUpdateCommand command) { - return of(command.getName(), command.getDate(), command.getTimeId(), command.getThemeId()); + return of(command.getMemberId(), command.getDate(), command.getTimeId(), command.getThemeId()); } - private Reservation of(String name, LocalDate date, Long timeId, Long themeId) { + private Reservation of(Long memberId, LocalDate date, Long timeId, Long themeId) { LocalDateTime now = LocalDateTime.now(clock); + Member member = members.getById(memberId); ReservationTime time = reservationTimes.getById(timeId); Theme theme = themes.getById(themeId); Slot slot = slots.findOrCreate(new ReservationDate(date), time, theme, now); - return Reservation.create(name, slot); + return Reservation.create(member, slot); } } diff --git a/src/main/java/roomescape/service/ReservationCreateCommand.java b/src/main/java/roomescape/service/ReservationCreateCommand.java index 421abce2e2..9020b305e4 100644 --- a/src/main/java/roomescape/service/ReservationCreateCommand.java +++ b/src/main/java/roomescape/service/ReservationCreateCommand.java @@ -5,30 +5,29 @@ import java.time.LocalDate; public class ReservationCreateCommand { - private final String name; + private final Long memberId; private final LocalDate date; private final Long timeId; private final Long themeId; - private ReservationCreateCommand(String name, LocalDate date, Long timeId, Long themeId) { - this.name = name; + private ReservationCreateCommand(Long memberId, LocalDate date, Long timeId, Long themeId) { + this.memberId = memberId; this.date = date; this.timeId = timeId; this.themeId = themeId; - } public static ReservationCreateCommand from(ReservationCreateRequest request) { return new ReservationCreateCommand( - request.getName(), + request.getMemberId(), request.getDate(), request.getTimeId(), request.getThemeId() ); } - public String getName() { - return name; + public Long getMemberId() { + return memberId; } public LocalDate getDate() { diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index b36b6bbda8..a8d3e6f50c 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -2,8 +2,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import roomescape.domain.RoomEscapeException; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; @@ -12,21 +14,26 @@ import java.time.Clock; import java.time.LocalDateTime; +import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; + @Service @Transactional(readOnly = true) public class ReservationService { private final Clock clock; private final ReservationAssembler assembler; private final ReservationRepository reservationRepository; + private final MemberRepository memberRepository; public ReservationService( Clock clock, ReservationAssembler assembler, - ReservationRepository reservationRepository + ReservationRepository reservationRepository, + MemberRepository memberRepository ) { this.clock = clock; this.assembler = assembler; this.reservationRepository = reservationRepository; + this.memberRepository = memberRepository; } @Transactional @@ -45,11 +52,20 @@ public Reservation find(long id) { return reservation.withRank(slotReservations.rankOf(reservation)); } - public Reservations findAll(String name) { - if (name == null) { + public Reservations findAll(Long memberId) { + if (memberId == null) { return new Reservations(reservationRepository.findAll()); } - return new Reservations(reservationRepository.findAllByName(new ReservationName(name))); + Member member = memberRepository.getById(memberId); + return new Reservations(reservationRepository.findAllByMember(member)); + } + + public Reservations findMine(Long memberId) { + if(!memberRepository.existsById(memberId)){ + throw new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 회원을 찾을 수 없습니다. : " + memberId); + } + + return new Reservations(reservationRepository.findMineWithDetails(memberId)); } @Transactional @@ -76,12 +92,12 @@ public Reservation update(ReservationUpdateCommand command, long id) { } @Transactional - public void cancel(long reservationId, String name) { + public void cancel(long reservationId, Long memberId) { Reservation reservation = reservationRepository.getById(reservationId); LocalDateTime now = LocalDateTime.now(clock); reservation.validateCancellable(now); - reservation.validateOwner(name); + reservation.validateOwner(memberId); reservationRepository.deleteById(reservationId); diff --git a/src/main/java/roomescape/service/ReservationUpdateCommand.java b/src/main/java/roomescape/service/ReservationUpdateCommand.java index 1eaeb6daf4..2c5eaf1cf7 100644 --- a/src/main/java/roomescape/service/ReservationUpdateCommand.java +++ b/src/main/java/roomescape/service/ReservationUpdateCommand.java @@ -5,13 +5,13 @@ import java.time.LocalDate; public class ReservationUpdateCommand { - private final String name; + private final Long memberId; private final LocalDate date; private final Long timeId; private final Long themeId; - public ReservationUpdateCommand(String name, LocalDate date, Long timeId, Long themeId) { - this.name = name; + public ReservationUpdateCommand(Long memberId, LocalDate date, Long timeId, Long themeId) { + this.memberId = memberId; this.date = date; this.timeId = timeId; this.themeId = themeId; @@ -19,15 +19,15 @@ public ReservationUpdateCommand(String name, LocalDate date, Long timeId, Long t public static ReservationUpdateCommand from(ReservationUpdateRequest request) { return new ReservationUpdateCommand( - request.getName(), + request.getMemberId(), request.getDate(), request.getTimeId(), request.getThemeId() ); } - public String getName() { - return name; + public Long getMemberId() { + return memberId; } public LocalDate getDate() { diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 896cf91596..224711ab64 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,26 +1,15 @@ -- RESERVATION_TIME: 10:00 ~ 20:00 (1시간 단위, 11개) -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('10:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('11:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('12:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('13:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('14:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('15:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('16:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('17:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('18:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('19:00'); -INSERT INTO RESERVATION_TIME (start_at) -VALUES ('20:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('10:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('11:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('12:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('13:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('14:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('15:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('16:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('17:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('18:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('19:00'); +INSERT INTO RESERVATION_TIME (start_at) VALUES ('20:00'); -- THEME: 5개 INSERT INTO THEME (name, description, thumbnail_url) @@ -37,144 +26,100 @@ INSERT INTO THEME (name, description, thumbnail_url) VALUES ('탐정 사무소', '미스터리 사건을 해결하세요', 'https://img.freepik.com/free-photo/private-detective-empty-workplace-with-crime-case-evidences-board-hanging-desk-police-investigator-office-surrounded-with-murder-scene-photos-clues-night-time_482257-59756.jpg?semt=ais_hybrid&w=740&q=80'); +-- MEMBER: 22명 +INSERT INTO MEMBER (name) VALUES ('김철수'); -- id=1 +INSERT INTO MEMBER (name) VALUES ('이영희'); -- id=2 +INSERT INTO MEMBER (name) VALUES ('박민수'); -- id=3 +INSERT INTO MEMBER (name) VALUES ('홍길동'); -- id=4 +INSERT INTO MEMBER (name) VALUES ('정수진'); -- id=5 +INSERT INTO MEMBER (name) VALUES ('한동훈'); -- id=6 +INSERT INTO MEMBER (name) VALUES ('임채원'); -- id=7 +INSERT INTO MEMBER (name) VALUES ('서태양'); -- id=8 +INSERT INTO MEMBER (name) VALUES ('유민호'); -- id=9 +INSERT INTO MEMBER (name) VALUES ('강민준'); -- id=10 +INSERT INTO MEMBER (name) VALUES ('조현아'); -- id=11 +INSERT INTO MEMBER (name) VALUES ('황준혁'); -- id=12 +INSERT INTO MEMBER (name) VALUES ('송미래'); -- id=13 +INSERT INTO MEMBER (name) VALUES ('안태양'); -- id=14 +INSERT INTO MEMBER (name) VALUES ('배소희'); -- id=15 +INSERT INTO MEMBER (name) VALUES ('권지훈'); -- id=16 +INSERT INTO MEMBER (name) VALUES ('류지아'); -- id=17 +INSERT INTO MEMBER (name) VALUES ('전현무'); -- id=18 +INSERT INTO MEMBER (name) VALUES ('표민혁'); -- id=19 +INSERT INTO MEMBER (name) VALUES ('대기자A'); -- id=20 +INSERT INTO MEMBER (name) VALUES ('대기자B'); -- id=21 +INSERT INTO MEMBER (name) VALUES ('대기자C'); -- id=22 + -- SLOT: 고유한 (date, time_id, theme_id) 조합 30개 -- theme_id=1 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-23', 1, 1); -- slot_id=1 (대기자 있음) -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-23', 2, 1); -- slot_id=2 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-24', 3, 1); -- slot_id=3 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-24', 4, 1); -- slot_id=4 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-25', 5, 1); -- slot_id=5 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-25', 6, 1); -- slot_id=6 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-26', 7, 1); -- slot_id=7 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-27', 8, 1); -- slot_id=8 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-28', 9, 1); -- slot_id=9 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-30', 10, 1); --- slot_id=10 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 1, 1); -- slot_id=1 (대기자 있음) +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 2, 1); -- slot_id=2 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 3, 1); -- slot_id=3 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 4, 1); -- slot_id=4 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 5, 1); -- slot_id=5 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 6, 1); -- slot_id=6 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 7, 1); -- slot_id=7 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 8, 1); -- slot_id=8 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 9, 1); -- slot_id=9 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 10, 1); -- slot_id=10 -- theme_id=2 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-23', 3, 2); -- slot_id=11 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-24', 4, 2); -- slot_id=12 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-25', 5, 2); -- slot_id=13 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-26', 6, 2); -- slot_id=14 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-26', 7, 2); -- slot_id=15 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-27', 8, 2); -- slot_id=16 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-28', 9, 2); -- slot_id=17 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-30', 10, 2); --- slot_id=18 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 3, 2); -- slot_id=11 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 4, 2); -- slot_id=12 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 5, 2); -- slot_id=13 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 6, 2); -- slot_id=14 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 7, 2); -- slot_id=15 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 8, 2); -- slot_id=16 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 9, 2); -- slot_id=17 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 10, 2); -- slot_id=18 -- theme_id=3 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-24', 1, 3); -- slot_id=19 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-25', 2, 3); -- slot_id=20 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-26', 3, 3); -- slot_id=21 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-27', 4, 3); -- slot_id=22 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-28', 5, 3); -- slot_id=23 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-30', 6, 3); --- slot_id=24 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 1, 3); -- slot_id=19 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 2, 3); -- slot_id=20 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 3, 3); -- slot_id=21 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 4, 3); -- slot_id=22 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 5, 3); -- slot_id=23 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 6, 3); -- slot_id=24 -- theme_id=4 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-25', 7, 4); -- slot_id=25 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-26', 8, 4); -- slot_id=26 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-27', 9, 4); -- slot_id=27 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-28', 10, 4); --- slot_id=28 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 7, 4); -- slot_id=25 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 8, 4); -- slot_id=26 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 9, 4); -- slot_id=27 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 10, 4); -- slot_id=28 -- theme_id=5 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-26', 11, 5); -- slot_id=29 -INSERT INTO SLOT (date, time_id, theme_id) -VALUES ('2026-05-30', 1, 5); --- slot_id=30 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 11, 5); -- slot_id=29 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 1, 5); -- slot_id=30 --- RESERVATION: 개별 예약자 (slot_id 참조) -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (1, '김철수', 'APPROVED', '2026-05-21 09:12:33'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (2, '이영희', 'APPROVED', '2026-05-21 11:45:07'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (3, '박민수', 'APPROVED', '2026-05-22 14:30:51'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (4, '홍길동', 'APPROVED', '2026-05-22 18:05:22'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (5, '정수진', 'APPROVED', '2026-05-23 21:40:18'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (6, '한동훈', 'APPROVED', '2026-05-24 08:15:44'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (7, '임채원', 'APPROVED', '2026-05-24 10:50:09'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (8, '서태양', 'APPROVED', '2026-05-25 13:22:37'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (9, '김철수', 'APPROVED', '2026-05-26 16:48:55'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (10, '유민호', 'APPROVED', '2026-05-28 20:11:02'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (11, '강민준', 'APPROVED', '2026-05-20 07:33:19'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (12, '조현아', 'APPROVED', '2026-05-22 09:58:41'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (13, '김철수', 'APPROVED', '2026-05-23 12:27:06'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (14, '홍길동', 'APPROVED', '2026-05-24 15:44:50'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (15, '황준혁', 'APPROVED', '2026-05-25 19:09:28'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (16, '송미래', 'APPROVED', '2026-05-26 08:41:13'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (17, '안태양', 'APPROVED', '2026-05-27 11:16:39'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (18, '배소희', 'APPROVED', '2026-05-29 14:52:04'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (19, '권지훈', 'APPROVED', '2026-05-22 17:30:47'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (20, '홍길동', 'APPROVED', '2026-05-23 20:55:21'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (21, '김철수', 'APPROVED', '2026-05-25 09:05:58'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (22, '류지아', 'APPROVED', '2026-05-26 12:38:16'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (23, '서태양', 'APPROVED', '2026-05-27 15:11:33'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (24, '서태양', 'APPROVED', '2026-05-29 18:47:09'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (25, '홍길동', 'APPROVED', '2026-05-23 08:23:42'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (26, '전현무', 'APPROVED', '2026-05-25 10:59:27'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (27, '서태양', 'APPROVED', '2026-05-26 13:34:50'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (28, '표민혁', 'APPROVED', '2026-05-27 16:20:15'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (29, '서태양', 'APPROVED', '2026-05-24 19:48:33'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (30, '홍길동', 'APPROVED', '2026-05-28 09:14:06'); +-- RESERVATION: member_id 참조 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (1, 1, 'APPROVED', '2026-05-21 09:12:33'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (2, 2, 'APPROVED', '2026-05-21 11:45:07'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (3, 3, 'APPROVED', '2026-05-22 14:30:51'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (4, 4, 'APPROVED', '2026-05-22 18:05:22'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (5, 5, 'APPROVED', '2026-05-23 21:40:18'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (6, 6, 'APPROVED', '2026-05-24 08:15:44'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (7, 7, 'APPROVED', '2026-05-24 10:50:09'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (8, 8, 'APPROVED', '2026-05-25 13:22:37'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (9, 1, 'APPROVED', '2026-05-26 16:48:55'); -- 김철수 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (10, 9, 'APPROVED', '2026-05-28 20:11:02'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (11, 10, 'APPROVED', '2026-05-20 07:33:19'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (12, 11, 'APPROVED', '2026-05-22 09:58:41'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (13, 1, 'APPROVED', '2026-05-23 12:27:06'); -- 김철수 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (14, 4, 'APPROVED', '2026-05-24 15:44:50'); -- 홍길동 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (15, 12, 'APPROVED', '2026-05-25 19:09:28'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (16, 13, 'APPROVED', '2026-05-26 08:41:13'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (17, 14, 'APPROVED', '2026-05-27 11:16:39'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (18, 15, 'APPROVED', '2026-05-29 14:52:04'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (19, 16, 'APPROVED', '2026-05-22 17:30:47'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (20, 4, 'APPROVED', '2026-05-23 20:55:21'); -- 홍길동 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (21, 1, 'APPROVED', '2026-05-25 09:05:58'); -- 김철수 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (22, 17, 'APPROVED', '2026-05-26 12:38:16'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (23, 8, 'APPROVED', '2026-05-27 15:11:33'); -- 서태양 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (24, 8, 'APPROVED', '2026-05-29 18:47:09'); -- 서태양 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (25, 4, 'APPROVED', '2026-05-23 08:23:42'); -- 홍길동 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (26, 18, 'APPROVED', '2026-05-25 10:59:27'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (27, 8, 'APPROVED', '2026-05-26 13:34:50'); -- 서태양 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (28, 19, 'APPROVED', '2026-05-27 16:20:15'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (29, 8, 'APPROVED', '2026-05-24 19:48:33'); -- 서태양 +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (30, 4, 'APPROVED', '2026-05-28 09:14:06'); -- 홍길동 -- 같은 슬롯(slot_id=1) 대기 테스트용 -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (1, '대기자A', 'WAITING', '2026-05-21 09:30:00'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (1, '대기자B', 'WAITING', '2026-05-21 10:00:00'); -INSERT INTO RESERVATION (slot_id, name, status, created_at) -VALUES (1, '대기자C', 'WAITING', '2026-05-21 10:30:00'); +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (1, 20, 'WAITING', '2026-05-21 09:30:00'); -- 대기자A +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (1, 21, 'WAITING', '2026-05-21 10:00:00'); -- 대기자B +INSERT INTO RESERVATION (slot_id, member_id, status, created_at) VALUES (1, 22, 'WAITING', '2026-05-21 10:30:00'); -- 대기자C diff --git a/src/test/java/roomescape/MissionStep2Test.java b/src/test/java/roomescape/MissionStep2Test.java index adcf4ed5ac..c46818283e 100644 --- a/src/test/java/roomescape/MissionStep2Test.java +++ b/src/test/java/roomescape/MissionStep2Test.java @@ -12,7 +12,6 @@ import java.sql.Connection; import java.sql.SQLException; -import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @@ -30,6 +29,7 @@ void init() { jdbcTemplate.update("insert into reservation_time(start_at) values ('10:00')"); jdbcTemplate.update( "insert into theme(name, description, thumbnail_url) values ('공포', '무서워요', 'https://zeze.com')"); + jdbcTemplate.update("insert into member(name) values ('브라운')"); // id=1 } @Test @@ -46,7 +46,7 @@ void init() { @Test void DB_조회_API_전환() { jdbcTemplate.update("INSERT INTO slot (date, time_id, theme_id) VALUES (?, ?, ?)", "2023-08-05", 1, 1); - jdbcTemplate.update("INSERT INTO reservation (slot_id, name, status) VALUES (?, ?, ?)", 1, "브라운", "APPROVED"); + jdbcTemplate.update("INSERT INTO reservation (slot_id, member_id, status) VALUES (?, ?, ?)", 1, 1, "APPROVED"); ReservationResponses reservations = RestAssured.given().log().all() .when().get("/reservations") @@ -65,11 +65,10 @@ void init() { @Test void DB_추가_삭제_API_전환() { Map params = new HashMap<>(); - params.put("name", "브라운"); + params.put("memberId", 1L); params.put("date", "2099-08-05"); params.put("timeId", 1); params.put("themeId", 1); - params.put("createdAt", LocalDateTime.now().toString()); RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -82,7 +81,7 @@ void init() { assertThat(count).isEqualTo(1); RestAssured.given().log().all() - .param("name", "브라운") + .param("memberId", 1) .when().delete("/reservations/1") .then().log().all() .statusCode(204); diff --git a/src/test/java/roomescape/MissionStep3Test.java b/src/test/java/roomescape/MissionStep3Test.java index fd8830cf75..7aa26af75b 100644 --- a/src/test/java/roomescape/MissionStep3Test.java +++ b/src/test/java/roomescape/MissionStep3Test.java @@ -49,9 +49,10 @@ public class MissionStep3Test { jdbcTemplate.update("insert into reservation_time(start_at) values ('10:00')"); jdbcTemplate.update( "insert into theme(name, description, thumbnail_url) values ('공포', '무서워요', 'https://zeze.com')"); + jdbcTemplate.update("insert into member(name) values ('브라운')"); // id=1 Map reservation = new HashMap<>(); - reservation.put("name", "브라운"); + reservation.put("memberId", 1L); reservation.put("date", "2099-08-05"); reservation.put("timeId", 1); reservation.put("themeId", 1); @@ -67,6 +68,6 @@ public class MissionStep3Test { .when().get("/reservations") .then().log().all() .statusCode(200) - .body("size()", is(1)); + .body("reservations.size()", is(1)); } } diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 093989a57c..f0f6529ccc 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -25,6 +25,7 @@ void init() { jdbcTemplate.update("insert into reservation_time(start_at) values ('10:00')"); jdbcTemplate.update( "insert into theme(name, description, thumbnail_url) values ('공포', '무서워요', 'https://zeze.com')"); + jdbcTemplate.update("insert into member(name) values ('브라운')"); // id=1 } @Test @@ -33,14 +34,14 @@ void init() { .when().get("/reservations") .then().log().all() .statusCode(200) - .body("reservations.size()", is(0)); // 아직 생성 요청이 없으니 0개 + .body("reservations.size()", is(0)); } @Test void 예약_추가_및_삭제() { Map params = new HashMap<>(); - params.put("name", "브라운"); + params.put("memberId", 1L); params.put("date", "2099-08-05"); params.put("timeId", 1); params.put("themeId", 1); @@ -60,7 +61,7 @@ void init() { .body("reservations.size()", is(1)); RestAssured.given().log().all() - .param("name", "브라운") + .param("memberId", 1) .when().delete("/reservations/1") .then().log().all() .statusCode(204); diff --git a/src/test/java/roomescape/RoomEscapeFixture.java b/src/test/java/roomescape/RoomEscapeFixture.java index e1849a52d9..54f0384869 100644 --- a/src/test/java/roomescape/RoomEscapeFixture.java +++ b/src/test/java/roomescape/RoomEscapeFixture.java @@ -1,8 +1,8 @@ package roomescape; +import roomescape.domain.member.Member; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; -import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; @@ -21,16 +21,20 @@ public class RoomEscapeFixture { Instant.parse("2026-05-10T03:00:00Z"), ZoneId.of("Asia/Seoul") ); - private static final ReservationName NAME = new ReservationName("zeze"); + private static final Member MEMBER = new Member(1L, "zeze"); private static final ReservationDate DATE = new ReservationDate(LocalDate.of(2099, 11, 11)); private static final ReservationTime TIME = ReservationTime.create(LocalTime.of(10, 0)); private static final Theme THEME = Theme.create(new ThemeName("공포"), "무서워요", new ThumbnailUrl("https://zeze.com")); + public static Member member() { + return MEMBER; + } + public static Slot slot() { return Slot.load(1L, DATE.getDate(), TIME, THEME); } public static Reservation reservation() { - return Reservation.create(NAME.getValue(), slot()).withStatus(Status.APPROVED); + return Reservation.create(MEMBER, slot()).withStatus(Status.APPROVED); } } diff --git a/src/test/java/roomescape/RoomescapeApplicationTest.java b/src/test/java/roomescape/RoomescapeApplicationTest.java index a61e1c2037..2e85909219 100644 --- a/src/test/java/roomescape/RoomescapeApplicationTest.java +++ b/src/test/java/roomescape/RoomescapeApplicationTest.java @@ -20,6 +20,9 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) class RoomescapeApplicationTest { private static final String AVAILABLE_DATE = "2099-06-01"; + private static final long ZEZE_ID = 1L; + private static final long MINGU_ID = 2L; + @Autowired private JdbcTemplate jdbcTemplate; @org.springframework.boot.test.web.server.LocalServerPort @@ -33,13 +36,15 @@ void init() { "insert into theme(name, description, thumbnail_url) values ('공포', '무서워요', 'https://zeze.com')"); jdbcTemplate.update( "insert into theme(name, description, thumbnail_url) values ('개그', '재밌어요', 'https://zeze.com')"); + jdbcTemplate.update("insert into member(name) values ('zeze')"); // id=1 + jdbcTemplate.update("insert into member(name) values ('mingu')"); // id=2 } @Test void 예약_생성_후_사용_시간_조회시_해당_시간이_제외된다() { int before = availableCount(AVAILABLE_DATE, 1); - reserve("제제", AVAILABLE_DATE, 1L, 1L, 201); + reserve(ZEZE_ID, AVAILABLE_DATE, 1L, 1L, 201); int after = availableCount(AVAILABLE_DATE, 1); assertThat(after).isEqualTo(before - 1); @@ -62,7 +67,7 @@ void init() { void 다른_테마_예약은_사용_시간_조회에_영향을_주지_않는다() { int before = availableCount(AVAILABLE_DATE, 1); - reserve("제제", AVAILABLE_DATE, 1L, 2L, 201); + reserve(ZEZE_ID, AVAILABLE_DATE, 1L, 2L, 201); int after = availableCount(AVAILABLE_DATE, 1); assertThat(after).isEqualTo(before); @@ -107,21 +112,15 @@ void init() { @Test void 중복_예약_수행시_409를_반환한다() { - String name = "zeze"; String date = "2099-05-14"; - long timeId = 1L; - long themeId = 1L; - reserve(name, date, timeId, themeId, 201); - reserve(name, date, timeId, themeId, 409); + reserve(ZEZE_ID, date, 1L, 1L, 201); + reserve(ZEZE_ID, date, 1L, 1L, 409); } @Test void 예약이_존재하는_시간을_지우면_409를_반환한다() { - String name = "zeze"; String date = "2099-05-14"; - long timeId = 1L; - long themeId = 1L; - reserve(name, date, timeId, themeId, 201); + reserve(ZEZE_ID, date, 1L, 1L, 201); RestAssured.given().log().all() .when().delete("/admin/times/1") @@ -139,19 +138,17 @@ void init() { @Test void 과거_예약_생성시_422를_반환한다() { - String past = "2020-01-01"; - - reserve("zeze", past, 1L, 1L, 422); + reserve(ZEZE_ID, "2020-01-01", 1L, 1L, 422); } @Test - void 이름으로_조회시_정상적으로_반환한다() { - reserve("zeze", "2099-05-01", 1L, 1L, 201); - reserve("zeze", "2099-05-02", 1L, 1L, 201); - reserve("zeze", "2099-05-03", 1L, 1L, 201); - reserve("mingu", "2099-05-04", 1L, 1L, 201); + void memberId로_조회시_정상적으로_반환한다() { + reserve(ZEZE_ID, "2099-05-01", 1L, 1L, 201); + reserve(ZEZE_ID, "2099-05-02", 1L, 1L, 201); + reserve(ZEZE_ID, "2099-05-03", 1L, 1L, 201); + reserve(MINGU_ID, "2099-05-04", 1L, 1L, 201); - RestAssured.given().params("name", "zeze") + RestAssured.given().params("memberId", ZEZE_ID) .when().get("/reservations") .then().log().all() .body("reservations.size()", is(3)); @@ -159,7 +156,7 @@ void init() { @Test void 예약_생성_후_단건_조회가_된다() { - int id = reserveAndGetId("zeze", "2099-06-01", 1L, 1L); + int id = reserveAndGetId(ZEZE_ID, "2099-06-01", 1L, 1L); RestAssured.given() .when().get("/reservations/" + id) @@ -170,8 +167,8 @@ void init() { @Test void 예약_생성_후_전체_목록에서_조회된다() { - reserve("zeze", "2099-06-01", 1L, 1L, 201); - reserve("mingu", "2099-06-02", 1L, 1L, 201); + reserve(ZEZE_ID, "2099-06-01", 1L, 1L, 201); + reserve(MINGU_ID, "2099-06-02", 1L, 1L, 201); RestAssured.given() .when().get("/reservations") @@ -181,7 +178,7 @@ void init() { @Test void 첫번째_예약은_승인_상태이다() { - int id = reserveAndGetId("zeze", "2099-06-01", 1L, 1L); + int id = reserveAndGetId(ZEZE_ID, "2099-06-01", 1L, 1L); RestAssured.given() .when().get("/reservations/" + id) @@ -192,8 +189,8 @@ void init() { @Test void 같은_슬롯에_두번째_예약은_대기_상태이다() { String date = "2099-06-10"; - reserveAndGetId("zeze", date, 1L, 1L); - int waitingId = reserveAndGetId("mingu", date, 1L, 1L); + reserveAndGetId(ZEZE_ID, date, 1L, 1L); + int waitingId = reserveAndGetId(MINGU_ID, date, 1L, 1L); RestAssured.given() .when().get("/reservations/" + waitingId) @@ -204,10 +201,10 @@ void init() { @Test void 예약_수정_성공한다() { - int id = reserveAndGetId("zeze", "2099-06-01", 1L, 1L); + int id = reserveAndGetId(ZEZE_ID, "2099-06-01", 1L, 1L); Map updateParams = new HashMap<>(); - updateParams.put("name", "zeze"); + updateParams.put("memberId", ZEZE_ID); updateParams.put("date", "2099-07-01"); updateParams.put("timeId", 1L); updateParams.put("themeId", 1L); @@ -222,10 +219,10 @@ void init() { @Test void 예약_삭제_성공한다() { - int id = reserveAndGetId("zeze", "2099-06-01", 1L, 1L); + int id = reserveAndGetId(ZEZE_ID, "2099-06-01", 1L, 1L); RestAssured.given() - .param("name", "zeze") + .param("memberId", ZEZE_ID) .when().delete("/reservations/" + id) .then().statusCode(204); @@ -235,17 +232,17 @@ void init() { } @Test - void 예약_삭제시_이름이_다르면_401을_반환한다() { - int id = reserveAndGetId("zeze", "2099-06-01", 1L, 1L); + void 예약_삭제시_회원ID가_다르면_403을_반환한다() { + int id = reserveAndGetId(ZEZE_ID, "2099-06-01", 1L, 1L); RestAssured.given() - .param("name", "other") + .param("memberId", 999L) .when().delete("/reservations/" + id) .then().statusCode(403); } @Test - void 예약_생성시_이름이_없으면_400을_반환한다() { + void 예약_생성시_memberId가_없으면_400을_반환한다() { Map params = new HashMap<>(); params.put("date", "2099-06-01"); params.put("timeId", 1L); @@ -261,7 +258,7 @@ void init() { @Test void 예약_생성시_timeId가_없으면_400을_반환한다() { Map params = new HashMap<>(); - params.put("name", "zeze"); + params.put("memberId", ZEZE_ID); params.put("date", "2099-06-01"); params.put("themeId", 1L); @@ -274,17 +271,17 @@ void init() { @Test void 존재하지_않는_시간으로_예약시_404를_반환한다() { - reserve("zeze", "2099-06-01", 999L, 1L, 404); + reserve(ZEZE_ID, "2099-06-01", 999L, 1L, 404); } @Test void 존재하지_않는_테마로_예약시_404를_반환한다() { - reserve("zeze", "2099-06-01", 1L, 999L, 404); + reserve(ZEZE_ID, "2099-06-01", 1L, 999L, 404); } - private int reserveAndGetId(String name, String date, Long timeId, Long themeId) { + private int reserveAndGetId(Long memberId, String date, Long timeId, Long themeId) { Map params = new HashMap<>(); - params.put("name", name); + params.put("memberId", memberId); params.put("date", date); params.put("timeId", timeId); params.put("themeId", themeId); @@ -305,9 +302,9 @@ private int availableCount(String date, long themeId) { .getTimes().size(); } - private void reserve(String name, String date, Long timeId, Long themeId, int expectedStatusCode) { + private void reserve(Long memberId, String date, Long timeId, Long themeId, int expectedStatusCode) { Map params = new HashMap<>(); - params.put("name", name); + params.put("memberId", memberId); params.put("date", date); params.put("timeId", timeId); params.put("themeId", themeId); @@ -318,4 +315,4 @@ private void reserve(String name, String date, Long timeId, Long themeId, int ex .when().post("/reservations") .then().statusCode(expectedStatusCode); } -} \ No newline at end of file +} diff --git a/src/test/java/roomescape/SlotReservationSyncTest.java b/src/test/java/roomescape/SlotReservationSyncTest.java index 82a6443f8b..76e19ef63e 100644 --- a/src/test/java/roomescape/SlotReservationSyncTest.java +++ b/src/test/java/roomescape/SlotReservationSyncTest.java @@ -7,6 +7,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.dao.DataAccessException; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationRepository; @@ -48,6 +50,7 @@ public class SlotReservationSyncTest { @Autowired private ReservationRepository reservationRepository; @Autowired private ReservationTimeRepository timeRepository; @Autowired private ThemeRepository themeRepository; + @Autowired private MemberRepository memberRepository; @Autowired private EntityManager em; private ReservationTime givenTime(int hour) { @@ -68,7 +71,8 @@ private Slot givenSlot(ReservationDate date, ReservationTime time, Theme theme) Theme theme = givenTheme("테스트 테마"); Slot slot = slotRepository.save(Slot.create(new ReservationDate(TODAY), time, theme, LocalDateTime.now(FIXED_CLOCK))); - Reservation reservation = Reservation.create("김철수", slot); + Member member = memberRepository.save(Member.create("김철수")); + Reservation reservation = Reservation.create(member, slot); slot.getReservations().add(reservation); @@ -85,7 +89,8 @@ private Slot givenSlot(ReservationDate date, ReservationTime time, Theme theme) Theme theme = givenTheme("테스트 테마"); Slot slot = slotRepository.save(Slot.create(new ReservationDate(TODAY), time, theme, LocalDateTime.now(FIXED_CLOCK))); - Reservation reservation = Reservation.create("김철수", slot); + Member member = memberRepository.save(Member.create("김철수")); + Reservation reservation = Reservation.create(member, slot); reservationRepository.save(reservation); diff --git a/src/test/java/roomescape/WriteBehindComparisonTest.java b/src/test/java/roomescape/WriteBehindComparisonTest.java index f465cf78b9..cdc70cc3a5 100644 --- a/src/test/java/roomescape/WriteBehindComparisonTest.java +++ b/src/test/java/roomescape/WriteBehindComparisonTest.java @@ -89,7 +89,7 @@ void sequence_defersInsert() { /** * 결정적 비교 — 여러 개 persist 후 한 번에 flush - * - SEQUENCE는 INSERT 3개가 flush 때 몰려서 나가고, IDENTITY는 persist마다 INSERT가 흩어져 나간다. + * - SEQUENCE는 INSERT 3개가 flush 때 몰려서 나가고, IDENTITY는 persist마다 INSERT가 흩어져 나간다. */ @Test @DisplayName("다건: SEQUENCE는 flush 때 INSERT 몰림, IDENTITY는 persist마다 흩어짐") diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java index bbe324b15b..b177485813 100644 --- a/src/test/java/roomescape/controller/ReservationControllerTest.java +++ b/src/test/java/roomescape/controller/ReservationControllerTest.java @@ -11,10 +11,12 @@ import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.domain.DomainErrorCode; import roomescape.domain.RoomEscapeException; +import roomescape.domain.member.Member; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; +import roomescape.domain.reservation.Status; import roomescape.domain.theme.Theme; import roomescape.service.ReservationService; @@ -24,7 +26,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -48,19 +49,19 @@ private Reservation approvedReservation() { ReservationTime time = ReservationTime.load(1L, LocalTime.of(10, 0)); Theme theme = Theme.load(1L, "공포", "무서워요", "https://zeze.com"); Slot slot = Slot.load(1L, LocalDate.of(2099, 1, 1), time, theme); - return Reservation.load(1L, "zeze", "APPROVED", slot); + return new Reservation(1L, new Member(1L, "zeze"), Status.APPROVED, slot); } private Reservation waitingReservation() { ReservationTime time = ReservationTime.load(1L, LocalTime.of(10, 0)); Theme theme = Theme.load(1L, "공포", "무서워요", "https://zeze.com"); Slot slot = Slot.load(1L, LocalDate.of(2099, 1, 1), time, theme); - return Reservation.load(2L, "mingu", "WAITING", slot); + return new Reservation(2L, new Member(2L, "mingu"), Status.WAITING, slot); } @Test void 예약_생성_성공시_201을_반환한다() throws Exception { - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.of(2099, 1, 1), 1L, 1L); + ReservationCreateRequest request = new ReservationCreateRequest(1L, LocalDate.of(2099, 1, 1), 1L, 1L); given(reservationService.reserve(any())).willReturn(approvedReservation()); mockMvc.perform(post("/reservations") @@ -73,7 +74,7 @@ private Reservation waitingReservation() { } @Test - void 예약_생성시_이름이_없으면_400을_반환한다() throws Exception { + void 예약_생성시_memberId가_없으면_400을_반환한다() throws Exception { ReservationCreateRequest request = new ReservationCreateRequest(null, LocalDate.of(2099, 1, 1), 1L, 1L); mockMvc.perform(post("/reservations") .contentType(MediaType.APPLICATION_JSON) @@ -83,7 +84,7 @@ private Reservation waitingReservation() { @Test void 예약_생성시_날짜가_없으면_400을_반환한다() throws Exception { - ReservationCreateRequest request = new ReservationCreateRequest("zeze", null, 1L, 1L); + ReservationCreateRequest request = new ReservationCreateRequest(1L, null, 1L, 1L); mockMvc.perform(post("/reservations") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) @@ -92,7 +93,7 @@ private Reservation waitingReservation() { @Test void 예약_생성시_TimeId가_없으면_400을_반환한다() throws Exception { - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.of(2099, 1, 1), null, 1L); + ReservationCreateRequest request = new ReservationCreateRequest(1L, LocalDate.of(2099, 1, 1), null, 1L); mockMvc.perform(post("/reservations") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) @@ -101,7 +102,7 @@ private Reservation waitingReservation() { @Test void 예약_생성시_서비스에서_중복_예외_발생시_409를_반환한다() throws Exception { - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.of(2099, 1, 1), 1L, 1L); + ReservationCreateRequest request = new ReservationCreateRequest(1L, LocalDate.of(2099, 1, 1), 1L, 1L); given(reservationService.reserve(any())) .willThrow(new RoomEscapeException(DomainErrorCode.ALREADY_EXISTS, "test")); mockMvc.perform(post("/reservations") @@ -112,7 +113,7 @@ private Reservation waitingReservation() { @Test void 예약_생성시_과거_날짜면_422를_반환한다() throws Exception { - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.of(2000, 1, 1), 1L, 1L); + ReservationCreateRequest request = new ReservationCreateRequest(1L, LocalDate.of(2000, 1, 1), 1L, 1L); given(reservationService.reserve(any())) .willThrow(new RoomEscapeException(DomainErrorCode.PAST_DATE, "test")); mockMvc.perform(post("/reservations") @@ -131,10 +132,10 @@ private Reservation waitingReservation() { } @Test - void 이름으로_예약_목록_조회_성공시_200을_반환한다() throws Exception { + void memberId로_예약_목록_조회_성공시_200을_반환한다() throws Exception { given(reservationService.findAll(any())) .willReturn(new Reservations(List.of(approvedReservation()))); - mockMvc.perform(get("/reservations").param("name", "zeze")) + mockMvc.perform(get("/reservations").param("memberId", "1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.reservations.length()").value(1)) .andExpect(jsonPath("$.reservations[0].name").value("zeze")); @@ -158,27 +159,27 @@ private Reservation waitingReservation() { @Test void 예약_삭제_성공시_204를_반환한다() throws Exception { - mockMvc.perform(delete("/reservations/1").param("name", "홍길동")) + mockMvc.perform(delete("/reservations/1").param("memberId", "1")) .andExpect(status().isNoContent()); } @Test - void 예약_삭제시_이름이_다르면_401을_반환한다() throws Exception { + void 예약_삭제시_회원ID가_다르면_403을_반환한다() throws Exception { willThrow(new RoomEscapeException(DomainErrorCode.FORBIDDEN, "test")) - .given(reservationService).cancel(anyLong(), anyString()); - mockMvc.perform(delete("/reservations/1").param("name", "other")) + .given(reservationService).cancel(anyLong(), anyLong()); + mockMvc.perform(delete("/reservations/1").param("memberId", "999")) .andExpect(status().isForbidden()); } @Test - void 예약_삭제시_이름이_없으면_400을_반환한다() throws Exception { + void 예약_삭제시_memberId가_없으면_400을_반환한다() throws Exception { mockMvc.perform(delete("/reservations/1")) .andExpect(status().isBadRequest()); } @Test void 예약_수정_성공시_200을_반환한다() throws Exception { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.of(2099, 6, 1), 1L, 1L); + ReservationUpdateRequest request = new ReservationUpdateRequest(1L, LocalDate.of(2099, 6, 1), 1L, 1L); given(reservationService.update(any(), anyLong())).willReturn(approvedReservation()); mockMvc.perform(put("/reservations/1") .contentType(MediaType.APPLICATION_JSON) @@ -189,7 +190,7 @@ private Reservation waitingReservation() { @Test void 예약_수정시_존재하지_않는_예약이면_404를_반환한다() throws Exception { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.of(2099, 6, 1), 1L, 1L); + ReservationUpdateRequest request = new ReservationUpdateRequest(1L, LocalDate.of(2099, 6, 1), 1L, 1L); given(reservationService.update(any(), anyLong())) .willThrow(new RoomEscapeException(DomainErrorCode.RESOURCE_NOT_FOUND, "test")); mockMvc.perform(put("/reservations/999") @@ -200,7 +201,7 @@ private Reservation waitingReservation() { @Test void 예약_수정시_과거_날짜면_422를_반환한다() throws Exception { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.of(2000, 1, 1), 1L, 1L); + ReservationUpdateRequest request = new ReservationUpdateRequest(1L, LocalDate.of(2000, 1, 1), 1L, 1L); given(reservationService.update(any(), anyLong())) .willThrow(new RoomEscapeException(DomainErrorCode.PAST_DATE, "test")); mockMvc.perform(put("/reservations/1") diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 36b91c1578..7646687b7e 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; import roomescape.domain.RoomEscapeException; +import roomescape.domain.member.Member; import roomescape.domain.theme.Theme; import java.time.LocalDate; @@ -19,16 +20,17 @@ private Slot validSlot() { } @Test - void 이름이_NULL이면_예외가_발생한다() { + void 회원이_NULL이면_예외가_발생한다() { assertThatThrownBy(() -> Reservation.create(null, validSlot())) .isInstanceOf(RoomEscapeException.class); } @Test void 정상적인_예약_생성은_성공한다() { - Reservation reservation = Reservation.create("zeze", validSlot()).withStatus(Status.APPROVED); + Member member = new Member(1L, "zeze"); + Reservation reservation = Reservation.create(member, validSlot()).withStatus(Status.APPROVED); - assertThat(reservation.getName().getValue()).isEqualTo("zeze"); + assertThat(reservation.getMember().getName()).isEqualTo("zeze"); assertThat(reservation.getStatus()).isEqualTo(Status.APPROVED); } } diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index e1fe0c4d27..d01b86d515 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -6,9 +6,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.dao.DataAccessException; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; -import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.ReservationTimeRepository; @@ -55,6 +56,9 @@ class ReservationRepositoryTest { @Autowired private ReservationRepository reservationRepository; + @Autowired + private MemberRepository memberRepository; + private ReservationTime givenTime(int hour) { return reservationTimeRepository.save(ReservationTime.create(LocalTime.of(hour, 0))); } @@ -67,34 +71,40 @@ private Slot givenSlot(ReservationDate date, ReservationTime time, Theme theme) return slotRepository.save(Slot.create(date, time, theme, LocalDateTime.now(FIXED_CLOCK))); } + private Member givenMember(String name) { + return memberRepository.save(Member.create(name)); + } + @Test @DisplayName("ID 부여하며 저장") void save() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation persisted = Reservation.create("유저", slot).withStatus(Status.APPROVED); + Member member = givenMember("유저"); + Reservation persisted = Reservation.create(member, slot).withStatus(Status.APPROVED); Reservation saved = reservationRepository.save(persisted); assertSoftly(softly -> { softly.assertThat(saved.getId()).isNotNull(); - softly.assertThat(saved.getName().getValue()).isEqualTo("유저"); + softly.assertThat(saved.getMember().getName()).isEqualTo("유저"); }); } @Test - @DisplayName("같은 이름 저장 시 유니크 처리") - void save_throwsException_whenSameSlotAndName() { + @DisplayName("같은 회원 저장 시 유니크 처리") + void save_throwsException_whenSameSlotAndMember() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation persisted = Reservation.create("유저", slot).withStatus(Status.APPROVED); + Member member = givenMember("유저"); + Reservation persisted = Reservation.create(member, slot).withStatus(Status.APPROVED); reservationRepository.save(persisted); assertThatThrownBy(() -> { - Reservation conflict = Reservation.create("유저", slot).withStatus(Status.WAITING); + Reservation conflict = Reservation.create(member, slot).withStatus(Status.WAITING); reservationRepository.saveAndFlush(conflict); }).isInstanceOf(DataAccessException.class); } @@ -105,8 +115,10 @@ void findAll() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given1 = Reservation.create("유저1", slot).withStatus(Status.APPROVED); - Reservation given2 = Reservation.create("유저2", slot).withStatus(Status.WAITING); + Member member1 = givenMember("유저1"); + Member member2 = givenMember("유저2"); + Reservation given1 = Reservation.create(member1, slot).withStatus(Status.APPROVED); + Reservation given2 = Reservation.create(member2, slot).withStatus(Status.WAITING); reservationRepository.save(given1); reservationRepository.save(given2); @@ -121,7 +133,8 @@ void findById() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given = Reservation.create("유저", slot).withStatus(Status.APPROVED); + Member member = givenMember("유저"); + Reservation given = Reservation.create(member, slot).withStatus(Status.APPROVED); Reservation saved = reservationRepository.save(given); Optional found = reservationRepository.findById(saved.getId()); @@ -129,7 +142,7 @@ void findById() { assertSoftly(softly -> { softly.assertThat(found).isPresent(); softly.assertThat(found.get().getId()).isEqualTo(saved.getId()); - softly.assertThat(found.get().getName()).isEqualTo(saved.getName()); + softly.assertThat(found.get().getMember().getId()).isEqualTo(saved.getMember().getId()); softly.assertThat(found.get().getStatus()).isEqualTo(saved.getStatus()); }); } @@ -140,7 +153,8 @@ void deleteById() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given = Reservation.create("유저", slot).withStatus(Status.APPROVED); + Member member = givenMember("유저"); + Reservation given = Reservation.create(member, slot).withStatus(Status.APPROVED); Reservation saved = reservationRepository.save(given); reservationRepository.deleteById(saved.getId()); @@ -154,8 +168,10 @@ void findBySlot_Id() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given1 = Reservation.create("유저1", slot).withStatus(Status.APPROVED); - Reservation given2 = Reservation.create("유저2", slot).withStatus(Status.WAITING); + Member member1 = givenMember("유저1"); + Member member2 = givenMember("유저2"); + Reservation given1 = Reservation.create(member1, slot).withStatus(Status.APPROVED); + Reservation given2 = Reservation.create(member2, slot).withStatus(Status.WAITING); reservationRepository.save(given1); reservationRepository.save(given2); @@ -165,16 +181,17 @@ void findBySlot_Id() { } @Test - @DisplayName("같은 slot_id와 name의 예약 존재 확인") - void existsBySlotIdAndName() { + @DisplayName("같은 slot_id와 member의 예약 존재 확인") + void existsBySlotIdAndMember() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given = Reservation.create("유저", slot).withStatus(Status.APPROVED); + Member member = givenMember("유저"); + Reservation given = Reservation.create(member, slot).withStatus(Status.APPROVED); Reservation saved = reservationRepository.save(given); List reservations = reservationRepository.findBySlot_Id(slot.getId()); - boolean exists = reservations.stream().anyMatch(r -> r.isSameName(saved)); + boolean exists = reservations.stream().anyMatch(r -> r.isSameMember(saved)); assertThat(exists).isTrue(); } @@ -185,7 +202,8 @@ void existsApprovedBySlotId() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given = Reservation.create("유저", slot).withStatus(Status.APPROVED); + Member member = givenMember("유저"); + Reservation given = Reservation.create(member, slot).withStatus(Status.APPROVED); reservationRepository.save(given); List reservations = reservationRepository.findBySlot_Id(slot.getId()); @@ -200,7 +218,8 @@ void changeStatus() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given = Reservation.create("유저", slot).withStatus(Status.WAITING); + Member member = givenMember("유저"); + Reservation given = Reservation.create(member, slot).withStatus(Status.WAITING); Reservation saved = reservationRepository.save(given); reservationRepository.getById(saved.getId()).changeStatus(Status.APPROVED); @@ -214,20 +233,20 @@ void changeStatus() { } @Test - @DisplayName("이름으로 예약 조회") - void findByName() { + @DisplayName("회원으로 예약 조회") + void findByMember() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given1 = Reservation.create("유저1", slot).withStatus(Status.APPROVED); - Reservation given2 = Reservation.create("유저2", slot).withStatus(Status.WAITING); - reservationRepository.save(given1); - reservationRepository.save(given2); + Member member1 = givenMember("유저1"); + Member member2 = givenMember("유저2"); + reservationRepository.save(Reservation.create(member1, slot).withStatus(Status.APPROVED)); + reservationRepository.save(Reservation.create(member2, slot).withStatus(Status.WAITING)); - List byName = reservationRepository.findAllByName(new ReservationName("유저1")); + List byMember = reservationRepository.findAllByMember(member1); - assertThat(byName).hasSize(1); - assertThat(byName.get(0).getName().getValue()).isEqualTo("유저1"); + assertThat(byMember).hasSize(1); + assertThat(byMember.get(0).getMember().getName()).isEqualTo("유저1"); } @Test @@ -236,7 +255,8 @@ void existsById_true() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Reservation given = Reservation.create("유저", slot).withStatus(Status.APPROVED); + Member member = givenMember("유저"); + Reservation given = Reservation.create(member, slot).withStatus(Status.APPROVED); Reservation saved = reservationRepository.save(given); assertThat(reservationRepository.existsById(saved.getId())).isTrue(); diff --git a/src/test/java/roomescape/repository/SlotRepositoryTest.java b/src/test/java/roomescape/repository/SlotRepositoryTest.java index c9a1373c04..44b9af3f15 100644 --- a/src/test/java/roomescape/repository/SlotRepositoryTest.java +++ b/src/test/java/roomescape/repository/SlotRepositoryTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationRepository; @@ -50,6 +52,9 @@ class SlotRepositoryTest { @Autowired private ReservationRepository reservationRepository; + @Autowired + private MemberRepository memberRepository; + private ReservationTime givenTime(int hour) { return reservationTimeRepository.save(ReservationTime.create(LocalTime.of(hour, 0))); } @@ -62,6 +67,10 @@ private Slot givenSlot(ReservationDate date, ReservationTime time, Theme theme) return slotRepository.save(Slot.create(date, time, theme, LocalDateTime.now(FIXED_CLOCK))); } + private Member givenMember(String name) { + return memberRepository.save(Member.create(name)); + } + @Test @DisplayName("ID 부여하며 저장") void save() { @@ -184,15 +193,17 @@ void existsByThemeId_false() { } @Test - @DisplayName("이름으로 슬롯 조회") - void findAllByName() { + @DisplayName("회원ID로 슬롯 조회") + void findAllByMemberId() { ReservationTime time = givenTime(14); Theme theme = givenTheme("테스트 테마"); Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - reservationRepository.save(Reservation.create("유저1", slot).withStatus(Status.APPROVED)); - reservationRepository.save(Reservation.create("유저2", slot).withStatus(Status.WAITING)); + Member member1 = givenMember("유저1"); + Member member2 = givenMember("유저2"); + reservationRepository.save(Reservation.create(member1, slot).withStatus(Status.APPROVED)); + reservationRepository.save(Reservation.create(member2, slot).withStatus(Status.WAITING)); - List slots = slotRepository.findAllByName("유저1"); + List slots = slotRepository.findAllByMemberId(member1.getId()); assertThat(slots).hasSize(1); assertThat(slots.get(0).getId()).isEqualTo(slot.getId()); diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index c0ad6e6cdd..c9ec6a2cde 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -11,11 +11,12 @@ import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.domain.DomainErrorCode; import roomescape.domain.RoomEscapeException; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationRepository; -import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Reservations; +import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; import roomescape.domain.theme.Theme; @@ -38,14 +39,16 @@ class ReservationServiceTest { private static final String URL = "https://zeze.com/thumb.jpg"; private static final String NAME = "제제"; + private static final long MEMBER_ID = 1L; + private static final Member DUMMY_MEMBER = new Member(MEMBER_ID, NAME); private static final Slot DUMMY_SLOT = Slot.load( 1L, LocalDate.of(2099, 1, 1), ReservationTime.load(1L, LocalTime.of(10, 0)), Theme.load(1L, "any", "any", URL) ); - private static final Reservation DUMMY = Reservation.load(1L, NAME, "APPROVED", DUMMY_SLOT); + private static final Reservation DUMMY = new Reservation(1L, DUMMY_MEMBER, Status.APPROVED, DUMMY_SLOT); private static final long NOT_EXISTS_ID = Long.MAX_VALUE; private static final long EXISTS_ID = 1L; @@ -56,6 +59,8 @@ class ReservationServiceTest { private ReservationAssembler assembler; @Mock private ReservationRepository reservationRepository; + @Mock + private MemberRepository memberRepository; @InjectMocks private ReservationService reservationService; @@ -69,21 +74,21 @@ private void givenNow(LocalDateTime dateTime) { givenNow(LocalDateTime.of(2026, 1, 1, 0, 0)); given(reservationRepository.getById(1L)).willReturn(DUMMY); given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of()); - reservationService.cancel(1L, NAME); + reservationService.cancel(1L, MEMBER_ID); verify(reservationRepository).deleteById(1L); } @Test void 존재하지_않는_예약_취소시_예외_발생() { given(reservationRepository.getById(999L)).willThrow(RoomEscapeException.class); - Assertions.assertThatThrownBy(() -> reservationService.cancel(999L, NAME)) + Assertions.assertThatThrownBy(() -> reservationService.cancel(999L, MEMBER_ID)) .isInstanceOf(RoomEscapeException.class); } @Test void 존재하지_않는_시간으로_예약시_예외() { given(assembler.from(any(ReservationCreateCommand.class))).willThrow(RoomEscapeException.class); - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.parse("2026-05-03"), 999L, 1L); + ReservationCreateRequest request = new ReservationCreateRequest(MEMBER_ID, LocalDate.parse("2026-05-03"), 999L, 1L); Assertions.assertThatThrownBy(() -> reservationService.reserve(ReservationCreateCommand.from(request))) .isInstanceOf(RoomEscapeException.class); } @@ -91,36 +96,37 @@ private void givenNow(LocalDateTime dateTime) { @Test void 지나간_날짜로_예약_시_예외가_발생해야_한다() { given(assembler.from(any(ReservationCreateCommand.class))).willThrow(new RoomEscapeException(DomainErrorCode.PAST_DATE, "test")); - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.parse("2026-04-05"), 1L, 1L); + ReservationCreateRequest request = new ReservationCreateRequest(MEMBER_ID, LocalDate.parse("2026-04-05"), 1L, 1L); Assertions.assertThatThrownBy(() -> reservationService.reserve(ReservationCreateCommand.from(request))) .isInstanceOf(RoomEscapeException.class); } @Test void 미래로_예약하면_성공해야_한다() { - Reservation assembled = Reservation.create(NAME, DUMMY_SLOT); + Reservation assembled = Reservation.create(DUMMY_MEMBER, DUMMY_SLOT); given(assembler.from(any(ReservationCreateCommand.class))).willReturn(assembled); given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of()); given(reservationRepository.save(any())).willReturn(DUMMY); Assertions.assertThatNoException().isThrownBy(() -> reservationService.reserve( - ReservationCreateCommand.from(new ReservationCreateRequest("zeze", LocalDate.parse("2026-04-05"), 1L, 1L)))); + ReservationCreateCommand.from(new ReservationCreateRequest(MEMBER_ID, LocalDate.parse("2026-04-05"), 1L, 1L)))); } @Test void 예약_생성시_이미_예약된_예약이면_예외가_발생한다() { - Reservation assembled = Reservation.create("zeze", DUMMY_SLOT); + Member zezeForConflict = new Member(1L, "zeze"); + Reservation assembled = Reservation.create(zezeForConflict, DUMMY_SLOT); given(assembler.from(any(ReservationCreateCommand.class))).willReturn(assembled); - Reservation existingReservation = Reservation.load(1L, "zeze", "APPROVED", DUMMY_SLOT); + Reservation existingReservation = new Reservation(1L, zezeForConflict, Status.APPROVED, DUMMY_SLOT); given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(existingReservation)); Assertions.assertThatThrownBy(() -> reservationService.reserve( - ReservationCreateCommand.from(new ReservationCreateRequest("zeze", LocalDate.parse("2099-04-05"), 1L, 1L)))) + ReservationCreateCommand.from(new ReservationCreateRequest(1L, LocalDate.parse("2099-04-05"), 1L, 1L)))) .isInstanceOf(RoomEscapeException.class); } @Test void 예약_수정시_ID가_없으면_예외가_발생한다() { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.parse("2099-04-06"), 1L, 1L); + ReservationUpdateRequest request = new ReservationUpdateRequest(MEMBER_ID, LocalDate.parse("2099-04-06"), 1L, 1L); given(reservationRepository.getById(999L)).willThrow(new RoomEscapeException(DomainErrorCode.RESOURCE_NOT_FOUND, "test")); Assertions.assertThatThrownBy(() -> reservationService.update(ReservationUpdateCommand.from(request), 999L)) .isInstanceOf(RoomEscapeException.class); @@ -128,7 +134,7 @@ private void givenNow(LocalDateTime dateTime) { @Test void 예약_수정시_과거_날짜의_예약이면_예외가_발생한다() { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.parse("2000-04-06"), 1L, 1L); + ReservationUpdateRequest request = new ReservationUpdateRequest(MEMBER_ID, LocalDate.parse("2000-04-06"), 1L, 1L); given(reservationRepository.getById(1L)).willReturn(DUMMY); given(assembler.from(any(ReservationUpdateCommand.class))).willThrow(new RoomEscapeException(DomainErrorCode.PAST_DATE, "test")); @@ -138,7 +144,7 @@ private void givenNow(LocalDateTime dateTime) { @Test void 예약_수정시_시간을_찾을_수_없으면_예외가_발생한다() { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.parse("2099-04-06"), 1L, 1L); + ReservationUpdateRequest request = new ReservationUpdateRequest(MEMBER_ID, LocalDate.parse("2099-04-06"), 1L, 1L); given(reservationRepository.getById(1L)).willReturn(DUMMY); given(assembler.from(any(ReservationUpdateCommand.class))).willThrow(new RoomEscapeException(DomainErrorCode.RESOURCE_NOT_FOUND, "test")); Assertions.assertThatThrownBy(() -> reservationService.update(ReservationUpdateCommand.from(request), 1L)) @@ -150,11 +156,11 @@ private void givenNow(LocalDateTime dateTime) { ReservationTime reservationTime = ReservationTime.load(1L, LocalTime.parse("11:00")); Theme theme = Theme.load(1L, "any", "any", URL); Slot newSlot = Slot.load(2L, LocalDate.parse("2099-04-06"), reservationTime, theme); - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.parse("2099-04-06"), 1L, 1L); + ReservationUpdateRequest request = new ReservationUpdateRequest(MEMBER_ID, LocalDate.parse("2099-04-06"), 1L, 1L); given(reservationRepository.getById(1L)).willReturn(DUMMY); - given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create("zeze", newSlot)); - Reservation conflicting = Reservation.load(2L, "zeze", "APPROVED", newSlot); + given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(DUMMY_MEMBER, newSlot)); + Reservation conflicting = new Reservation(2L, DUMMY_MEMBER, Status.APPROVED, newSlot); given(reservationRepository.findBySlot_Id(2L)).willReturn(List.of(conflicting)); Assertions.assertThatThrownBy(() -> reservationService.update(ReservationUpdateCommand.from(request), 1L)) @@ -165,17 +171,16 @@ private void givenNow(LocalDateTime dateTime) { void 예약_수정시_같은_슬롯이면_자기_자신이므로_중복이_아니어야_한다() { long timeId = 1L; long themeId = 1L; - String name = "zeze"; ReservationTime time = ReservationTime.load(timeId, LocalTime.of(10, 0)); Theme theme = Theme.load(themeId, "any", "any", URL); Slot slot = Slot.load(1L, LocalDate.of(2099, 1, 1), time, theme); - Reservation existing = Reservation.load(1L, name, "APPROVED", slot); + Reservation existing = new Reservation(1L, DUMMY_MEMBER, Status.APPROVED, slot); - ReservationUpdateRequest request = new ReservationUpdateRequest(name, LocalDate.of(2099, 1, 1), timeId, themeId); + ReservationUpdateRequest request = new ReservationUpdateRequest(MEMBER_ID, LocalDate.of(2099, 1, 1), timeId, themeId); given(reservationRepository.getById(1L)).willReturn(existing); - given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(name, slot)); + given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(DUMMY_MEMBER, slot)); given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(existing)); assertThatCode(() -> reservationService.update(ReservationUpdateCommand.from(request), 1L)) @@ -185,16 +190,16 @@ private void givenNow(LocalDateTime dateTime) { @Test void 예약_삭제_시_ID가_존재하지_않으면_예외가_발생한다() { given(reservationRepository.getById(NOT_EXISTS_ID)).willThrow(RoomEscapeException.class); - Assertions.assertThatThrownBy(() -> reservationService.cancel(NOT_EXISTS_ID, NAME)) + Assertions.assertThatThrownBy(() -> reservationService.cancel(NOT_EXISTS_ID, MEMBER_ID)) .isInstanceOf(RoomEscapeException.class); } @Test - void 예약_삭제_시_이름이_다르면_예외가_발생한다() { + void 예약_삭제_시_회원ID가_다르면_예외가_발생한다() { givenNow(LocalDateTime.of(2026, 1, 1, 0, 0)); Reservation reservation = RoomEscapeFixture.reservation(); given(reservationRepository.getById(EXISTS_ID)).willReturn(reservation); - Assertions.assertThatThrownBy(() -> reservationService.cancel(EXISTS_ID, "diff")) + Assertions.assertThatThrownBy(() -> reservationService.cancel(EXISTS_ID, 999L)) .isInstanceOf(RoomEscapeException.class); } @@ -204,20 +209,20 @@ private void givenNow(LocalDateTime dateTime) { Reservation reservation = RoomEscapeFixture.reservation(); given(reservationRepository.getById(EXISTS_ID)).willReturn(reservation); given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of()); - assertThatCode(() -> reservationService.cancel(EXISTS_ID, reservation.getName().getValue())) + assertThatCode(() -> reservationService.cancel(EXISTS_ID, reservation.getMember().getId())) .doesNotThrowAnyException(); } @Test void 승인된_예약_취소_시_첫_번째_대기자가_승급된다() { givenNow(LocalDateTime.of(2026, 1, 1, 0, 0)); - Reservation approved = Reservation.load(1L, NAME, "APPROVED", DUMMY_SLOT); - Reservation waiting = Reservation.load(2L, "대기자", "WAITING", DUMMY_SLOT); + Reservation approved = new Reservation(1L, DUMMY_MEMBER, Status.APPROVED, DUMMY_SLOT); + Reservation waiting = new Reservation(2L, new Member(2L, "대기자"), Status.WAITING, DUMMY_SLOT); given(reservationRepository.getById(1L)).willReturn(approved); given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(waiting)); - reservationService.cancel(1L, NAME); + reservationService.cancel(1L, MEMBER_ID); assertThat(waiting.getStatus()).isEqualTo(Status.APPROVED); } @@ -225,10 +230,10 @@ private void givenNow(LocalDateTime dateTime) { @Test void 대기_예약_취소_시_승급이_일어나지_않는다() { givenNow(LocalDateTime.of(2026, 1, 1, 0, 0)); - Reservation waiting = Reservation.load(1L, NAME, "WAITING", DUMMY_SLOT); + Reservation waiting = new Reservation(1L, DUMMY_MEMBER, Status.WAITING, DUMMY_SLOT); given(reservationRepository.getById(1L)).willReturn(waiting); - reservationService.cancel(1L, NAME); + reservationService.cancel(1L, MEMBER_ID); verify(reservationRepository, never()).findBySlot_Id(any()); } @@ -236,29 +241,29 @@ private void givenNow(LocalDateTime dateTime) { @Test void 승인된_예약의_슬롯_변경_시_기존_슬롯의_첫_번째_대기자가_승급된다() { Slot newSlot = Slot.load(2L, LocalDate.of(2099, 6, 1), ReservationTime.load(1L, LocalTime.of(11, 0)), Theme.load(1L, "any", "any", URL)); - Reservation existing = Reservation.load(1L, NAME, "APPROVED", DUMMY_SLOT); - Reservation waitingInOldSlot = Reservation.load(3L, "대기자", "WAITING", DUMMY_SLOT); + Reservation existing = new Reservation(1L, DUMMY_MEMBER, Status.APPROVED, DUMMY_SLOT); + Reservation waitingInOldSlot = new Reservation(3L, new Member(3L, "대기자"), Status.WAITING, DUMMY_SLOT); given(reservationRepository.getById(1L)).willReturn(existing); - given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(NAME, newSlot)); + given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(DUMMY_MEMBER, newSlot)); given(reservationRepository.findBySlot_Id(2L)).willReturn(List.of()); given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(waitingInOldSlot)); - reservationService.update(ReservationUpdateCommand.from(new ReservationUpdateRequest(NAME, LocalDate.of(2099, 6, 1), 1L, 1L)), 1L); + reservationService.update(ReservationUpdateCommand.from(new ReservationUpdateRequest(MEMBER_ID, LocalDate.of(2099, 6, 1), 1L, 1L)), 1L); assertThat(waitingInOldSlot.getStatus()).isEqualTo(Status.APPROVED); } @Test void 승인된_예약의_슬롯_미변경_시_승급이_일어나지_않는다() { - Reservation existing = Reservation.load(1L, NAME, "APPROVED", DUMMY_SLOT); + Reservation existing = new Reservation(1L, DUMMY_MEMBER, Status.APPROVED, DUMMY_SLOT); given(reservationRepository.getById(1L)).willReturn(existing); - given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(NAME, DUMMY_SLOT)); + given(assembler.from(any(ReservationUpdateCommand.class))).willReturn(Reservation.create(DUMMY_MEMBER, DUMMY_SLOT)); given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(existing)); assertThatCode(() -> reservationService.update( - ReservationUpdateCommand.from(new ReservationUpdateRequest(NAME, LocalDate.of(2099, 1, 1), 1L, 1L)), 1L)) + ReservationUpdateCommand.from(new ReservationUpdateRequest(MEMBER_ID, LocalDate.of(2099, 1, 1), 1L, 1L)), 1L)) .doesNotThrowAnyException(); } @@ -279,7 +284,7 @@ private void givenNow(LocalDateTime dateTime) { } @Test - void 이름_없이_목록_조회시_전체_예약을_반환한다() { + void memberId_없이_목록_조회시_전체_예약을_반환한다() { given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); Reservations results = reservationService.findAll(null); @@ -289,13 +294,14 @@ private void givenNow(LocalDateTime dateTime) { } @Test - void 이름으로_목록_조회시_해당_이름의_예약만_반환한다() { - given(reservationRepository.findAllByName(new ReservationName(NAME))).willReturn(List.of(DUMMY)); + void memberId로_목록_조회시_해당_회원의_예약만_반환한다() { + given(memberRepository.getById(MEMBER_ID)).willReturn(DUMMY_MEMBER); + given(reservationRepository.findAllByMember(DUMMY_MEMBER)).willReturn(List.of(DUMMY)); - Reservations results = reservationService.findAll(NAME); + Reservations results = reservationService.findAll(MEMBER_ID); Assertions.assertThat(results.getValues()).hasSize(1); - Assertions.assertThat(results.getValues().get(0).getName().getValue()).isEqualTo(NAME); + Assertions.assertThat(results.getValues().get(0).getMember().getName()).isEqualTo(NAME); } @Test @@ -306,23 +312,4 @@ private void givenNow(LocalDateTime dateTime) { Assertions.assertThat(result.getStatus()).isEqualTo(Status.APPROVED); } - - @Test - void 두번째_이후_예약은_대기_상태이고_대기번호_1번이다() { - Slot waitingSlot = Slot.load( - 1L, - LocalDate.of(2099, 1, 1), - ReservationTime.load(1L, LocalTime.of(10, 0)), - Theme.load(1L, "any", "any", URL) - ); - Reservation approved = Reservation.load(1L, NAME, "APPROVED", waitingSlot); - Reservation waiting = Reservation.load(2L, "대기자", "WAITING", waitingSlot); - given(reservationRepository.getById(2L)).willReturn(waiting); - given(reservationRepository.findBySlot_Id(1L)).willReturn(List.of(approved, waiting)); - - Reservation result = reservationService.find(2L); - - Assertions.assertThat(result.getStatus()).isEqualTo(Status.WAITING); - Assertions.assertThat(result.getRank().getValue()).isEqualTo(1); - } } From 60f1802559a3eae9c2f191dcc013db8a795bbb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=88=98=ED=98=84?= Date: Sun, 21 Jun 2026 23:48:55 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[2=EB=8B=A8=EA=B3=84]=20jpql=20entitygraph?= =?UTF-8?q?=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 --- .../roomescape/domain/reservation/ReservationRepository.java | 2 ++ src/main/java/roomescape/domain/reservation/Slot.java | 2 +- src/main/java/roomescape/service/ReservationService.java | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index a6780faedd..240bb1b5e1 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -1,5 +1,6 @@ package roomescape.domain.reservation; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -15,6 +16,7 @@ public interface ReservationRepository extends JpaRepository List findBySlot_Id(Long slotId); + @EntityGraph(attributePaths = {"member", "slot", "slot.theme", "slot.time"}) List findByMemberId(Long memberId); @Query("select r from Reservation r " + diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index c1a1e99e69..1c53fd7bd4 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -37,7 +37,7 @@ public class Slot { @Embedded private ReservationDate date; - @ManyToOne(optional = false, fetch = FetchType.LAZY) + @ManyToOne(optional = true, fetch = FetchType.LAZY) @JoinColumn(name = "time_id") private ReservationTime time; diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index a8d3e6f50c..985f5e545c 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -65,7 +65,7 @@ public Reservations findMine(Long memberId) { throw new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 회원을 찾을 수 없습니다. : " + memberId); } - return new Reservations(reservationRepository.findMineWithDetails(memberId)); + return new Reservations(reservationRepository.findByMemberId(memberId)); } @Transactional From 55fb38d0514e0be665298b75d6c320a20f27359d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=88=98=ED=98=84?= Date: Mon, 22 Jun 2026 03:09:48 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[3=20=EB=8B=A8=EA=B3=84]=20Rank=20jpql=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReservationController.java | 12 +++-- .../dto/response/ReservationResponse.java | 17 +++++- .../dto/response/ReservationResponses.java | 7 +++ .../domain/reservation/Reservation.java | 14 ----- .../reservation/ReservationRepository.java | 53 ++++++++++++++++--- .../reservation/ReservationWithRank.java | 4 ++ .../domain/reservation/Reservations.java | 15 +++--- .../service/ReservationService.java | 26 ++++----- .../controller/ReservationControllerTest.java | 19 +++++-- .../service/ReservationServiceTest.java | 17 +++--- 10 files changed, 116 insertions(+), 68 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservation/ReservationWithRank.java diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java index a50ab706d6..04448c42c0 100644 --- a/src/main/java/roomescape/controller/ReservationController.java +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -16,12 +16,14 @@ import roomescape.controller.dto.response.ReservationResponse; import roomescape.controller.dto.response.ReservationResponses; import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationWithRank; import roomescape.domain.reservation.Reservations; import roomescape.service.ReservationCreateCommand; import roomescape.service.ReservationService; import roomescape.service.ReservationUpdateCommand; import java.net.URI; +import java.util.List; @RestController public class ReservationController { @@ -54,14 +56,14 @@ public ResponseEntity findList( @GetMapping("/reservations/{id}") public ResponseEntity find(@PathVariable long id) { - Reservation reservation = reservationService.find(id); - return ResponseEntity.ok(ReservationResponse.toDto(reservation)); + ReservationWithRank reservationWithRank = reservationService.find(id); + return ResponseEntity.ok(ReservationResponse.toDto(reservationWithRank)); } @GetMapping("/reservations-mine") public ResponseEntity findMine(@RequestParam Long memberId) { - Reservations reservations = reservationService.findMine(memberId); - return ResponseEntity.ok(ReservationResponses.toDto(reservations)); + List reservations = reservationService.findMine(memberId); + return ResponseEntity.ok(ReservationResponses.toDtoWithRank(reservations)); } @DeleteMapping("/reservations/{id}") @@ -78,7 +80,7 @@ public ResponseEntity update( @Valid @RequestBody ReservationUpdateRequest request, @PathVariable long id ) { - Reservation updated = reservationService.update(ReservationUpdateCommand.from(request), id); + ReservationWithRank updated = reservationService.update(ReservationUpdateCommand.from(request), id); return ResponseEntity.ok(ReservationResponse.toDto(updated)); } } diff --git a/src/main/java/roomescape/controller/dto/response/ReservationResponse.java b/src/main/java/roomescape/controller/dto/response/ReservationResponse.java index 5b188cb891..dfa80951c3 100644 --- a/src/main/java/roomescape/controller/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/controller/dto/response/ReservationResponse.java @@ -1,6 +1,7 @@ package roomescape.controller.dto.response; import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationWithRank; import roomescape.domain.reservation.Slot; import java.time.LocalDate; @@ -27,13 +28,25 @@ public ReservationResponse(long id, String name, LocalDate date, String state, L public static ReservationResponse toDto(Reservation reservation) { Slot slot = reservation.getSlot(); - Long rank = reservation.getRank() != null ? reservation.getRank().getValue() : null; return new ReservationResponse( reservation.getId(), reservation.getMember().getName(), slot.getDate().getDate(), reservation.getStatus().getKoreanName(), - rank, + null, + ReservationTimeResponse.toDto(slot.getTime()), + ThemeResponse.toDto(slot.getTheme())); + } + + public static ReservationResponse toDto(ReservationWithRank reservationWithRank) { + Reservation reservation = reservationWithRank.reservation(); + Slot slot = reservation.getSlot(); + return new ReservationResponse( + reservation.getId(), + reservation.getMember().getName(), + slot.getDate().getDate(), + reservation.getStatus().getKoreanName(), + reservationWithRank.rank(), ReservationTimeResponse.toDto(slot.getTime()), ThemeResponse.toDto(slot.getTheme())); } diff --git a/src/main/java/roomescape/controller/dto/response/ReservationResponses.java b/src/main/java/roomescape/controller/dto/response/ReservationResponses.java index d1d7e2b257..31d2326560 100644 --- a/src/main/java/roomescape/controller/dto/response/ReservationResponses.java +++ b/src/main/java/roomescape/controller/dto/response/ReservationResponses.java @@ -1,5 +1,6 @@ package roomescape.controller.dto.response; +import roomescape.domain.reservation.ReservationWithRank; import roomescape.domain.reservation.Reservations; import java.util.List; @@ -18,6 +19,12 @@ public static ReservationResponses toDto(Reservations reservations) { .toList()); } + public static ReservationResponses toDtoWithRank(List reservations) { + return new ReservationResponses(reservations.stream() + .map(ReservationResponse::toDto) + .toList()); + } + public List getReservations() { return reservations; } diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 87d799c1c3..2f97d83202 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -11,7 +11,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import jakarta.persistence.Transient; import jakarta.persistence.UniqueConstraint; import roomescape.domain.DomainErrorCode; import roomescape.domain.RoomEscapeException; @@ -41,9 +40,6 @@ public class Reservation { @JoinColumn(name = "slot_id") private Slot slot; - @Transient - private Rank rank; - @Column(name = "created_at", insertable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") private LocalDateTime createdAt; @@ -69,12 +65,6 @@ public Reservation withStatus(Status status) { return new Reservation(id, member, status, slot); } - public Reservation withRank(Rank rank) { - Reservation copy = new Reservation(id, member, status, slot); - copy.rank = rank; - return copy; - } - public void changeStatus(Status status) { this.status = status; } @@ -125,10 +115,6 @@ public Long getSlotId() { return slot.getId(); } - public Rank getRank() { - return rank; - } - @Override public final boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index 240bb1b5e1..0ff06f4dc9 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -8,6 +8,9 @@ import roomescape.domain.member.Member; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; @@ -16,19 +19,53 @@ public interface ReservationRepository extends JpaRepository List findBySlot_Id(Long slotId); - @EntityGraph(attributePaths = {"member", "slot", "slot.theme", "slot.time"}) + @EntityGraph(attributePaths = {"member", "slot", "slot.time", "slot.theme"}) + Optional findById(Long id); + + @EntityGraph(attributePaths = {"member", "slot", "slot.time", "slot.theme"}) List findByMemberId(Long memberId); - @Query("select r from Reservation r " + - "join fetch r.member " + - "join fetch r.slot s " + - "join fetch s.theme " + - "join fetch s.time " + - "where r.member.id = :memberId") - List findMineWithDetails(@Param("memberId") Long memberId); + @Query(""" + SELECT CASE WHEN r.status = roomescape.domain.reservation.Status.APPROVED THEN 0L + ELSE (SELECT COUNT(w) + 1L FROM Reservation w + WHERE w.slot = r.slot + AND w.status = roomescape.domain.reservation.Status.WAITING + AND w.createdAt < r.createdAt) + END + FROM Reservation r WHERE r.id = :id + """) + Long findRankById(@Param("id") Long id); + + @Query(""" + SELECT r.id, + CASE WHEN r.status = roomescape.domain.reservation.Status.APPROVED THEN 0L + ELSE (SELECT COUNT(w) + 1L FROM Reservation w + WHERE w.slot = r.slot + AND w.status = roomescape.domain.reservation.Status.WAITING + AND w.createdAt < r.createdAt) + END + FROM Reservation r WHERE r.member.id = :memberId + """) + List findRanksByMemberId(@Param("memberId") Long memberId); default Reservation getById(Long id) { return findById(id) .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 예약을 찾을 수 없습니다. : " + id)); } + + default ReservationWithRank getByIdWithRank(Long id) { + Reservation reservation = findById(id) + .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 예약을 찾을 수 없습니다. : " + id)); + return new ReservationWithRank(reservation, findRankById(id)); + } + + default List findAllByMemberIdWithRank(Long memberId) { + Reservations reservations = new Reservations(findByMemberId(memberId)); + Map rankMap = findRanksByMemberId(memberId).stream() + .collect(Collectors.toMap( + row -> (Long) row[0], + row -> (Long) row[1] + )); + return reservations.withRanks(rankMap); + } } diff --git a/src/main/java/roomescape/domain/reservation/ReservationWithRank.java b/src/main/java/roomescape/domain/reservation/ReservationWithRank.java new file mode 100644 index 0000000000..f444595352 --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/ReservationWithRank.java @@ -0,0 +1,4 @@ +package roomescape.domain.reservation; + +public record ReservationWithRank(Reservation reservation, long rank) { +} diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java index 5fe6079ba9..f824a765c2 100644 --- a/src/main/java/roomescape/domain/reservation/Reservations.java +++ b/src/main/java/roomescape/domain/reservation/Reservations.java @@ -4,6 +4,7 @@ import roomescape.domain.RoomEscapeException; import java.util.List; +import java.util.Map; import java.util.Optional; public class Reservations { @@ -15,8 +16,7 @@ public Reservations(List values) { public Reservation join(Reservation assembled) { conflictByMember(assembled); - Reservation withStatus = assembled.withStatus(nextStatus()); - return withStatus.withRank(rankOf(withStatus)); + return assembled.withStatus(nextStatus()); } private Status nextStatus() { @@ -44,13 +44,10 @@ public Optional firstWaiting() { .findFirst(); } - public Rank rankOf(Reservation reservation) { - if (reservation.isApproved()) { - return new Rank(0); - } - List waitings = values.stream().filter(Reservation::isWaiting).toList(); - int position = waitings.indexOf(reservation); - return position == -1 ? new Rank(waitings.size() + 1) : new Rank(position + 1); + public List withRanks(Map rankMap) { + return values.stream() + .map(r -> new ReservationWithRank(r, rankMap.getOrDefault(r.getId(), 0L))) + .toList(); } public List getValues() { diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 985f5e545c..603fe4bac7 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -2,19 +2,18 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import roomescape.domain.RoomEscapeException; import roomescape.domain.member.Member; import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; +import roomescape.domain.reservation.ReservationWithRank; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; import java.time.Clock; import java.time.LocalDateTime; - -import static roomescape.domain.DomainErrorCode.RESOURCE_NOT_FOUND; +import java.util.List; @Service @Transactional(readOnly = true) @@ -42,14 +41,12 @@ public Reservation reserve(ReservationCreateCommand command) { Slot slot = assembled.getSlot(); Reservations existing = new Reservations(reservationRepository.findBySlot_Id(slot.getId())); - Reservation join = existing.join(assembled); - return reservationRepository.save(join); + Reservation joined = existing.join(assembled); + return reservationRepository.save(joined); } - public Reservation find(long id) { - Reservation reservation = reservationRepository.getById(id); - Reservations slotReservations = new Reservations(reservationRepository.findBySlot_Id(reservation.getSlotId())); - return reservation.withRank(slotReservations.rankOf(reservation)); + public ReservationWithRank find(long id) { + return reservationRepository.getByIdWithRank(id); } public Reservations findAll(Long memberId) { @@ -60,16 +57,13 @@ public Reservations findAll(Long memberId) { return new Reservations(reservationRepository.findAllByMember(member)); } - public Reservations findMine(Long memberId) { - if(!memberRepository.existsById(memberId)){ - throw new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 회원을 찾을 수 없습니다. : " + memberId); - } - - return new Reservations(reservationRepository.findByMemberId(memberId)); + public List findMine(Long memberId) { + memberRepository.getById(memberId); // 존재 여부 확인 + return reservationRepository.findAllByMemberIdWithRank(memberId); } @Transactional - public Reservation update(ReservationUpdateCommand command, long id) { + public ReservationWithRank update(ReservationUpdateCommand command, long id) { Reservation existing = reservationRepository.getById(id); Reservation assembled = assembler.from(command); diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java index b177485813..4dd7b4f55a 100644 --- a/src/test/java/roomescape/controller/ReservationControllerTest.java +++ b/src/test/java/roomescape/controller/ReservationControllerTest.java @@ -14,6 +14,7 @@ import roomescape.domain.member.Member; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationTime; +import roomescape.domain.reservation.ReservationWithRank; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; @@ -45,24 +46,32 @@ class ReservationControllerTest { @MockitoBean private ReservationService reservationService; - private Reservation approvedReservation() { + private Reservation approvedReservationEntity() { ReservationTime time = ReservationTime.load(1L, LocalTime.of(10, 0)); Theme theme = Theme.load(1L, "공포", "무서워요", "https://zeze.com"); Slot slot = Slot.load(1L, LocalDate.of(2099, 1, 1), time, theme); return new Reservation(1L, new Member(1L, "zeze"), Status.APPROVED, slot); } - private Reservation waitingReservation() { + private Reservation waitingReservationEntity() { ReservationTime time = ReservationTime.load(1L, LocalTime.of(10, 0)); Theme theme = Theme.load(1L, "공포", "무서워요", "https://zeze.com"); Slot slot = Slot.load(1L, LocalDate.of(2099, 1, 1), time, theme); return new Reservation(2L, new Member(2L, "mingu"), Status.WAITING, slot); } + private ReservationWithRank approvedReservation() { + return new ReservationWithRank(approvedReservationEntity(), 0L); + } + + private ReservationWithRank waitingReservation() { + return new ReservationWithRank(waitingReservationEntity(), 1L); + } + @Test void 예약_생성_성공시_201을_반환한다() throws Exception { ReservationCreateRequest request = new ReservationCreateRequest(1L, LocalDate.of(2099, 1, 1), 1L, 1L); - given(reservationService.reserve(any())).willReturn(approvedReservation()); + given(reservationService.reserve(any())).willReturn(approvedReservationEntity()); mockMvc.perform(post("/reservations") .contentType(MediaType.APPLICATION_JSON) @@ -125,7 +134,7 @@ private Reservation waitingReservation() { @Test void 예약_전체_목록_조회_성공시_200을_반환한다() throws Exception { given(reservationService.findAll(any())) - .willReturn(new Reservations(List.of(approvedReservation(), waitingReservation()))); + .willReturn(new Reservations(List.of(approvedReservationEntity(), waitingReservationEntity()))); mockMvc.perform(get("/reservations")) .andExpect(status().isOk()) .andExpect(jsonPath("$.reservations.length()").value(2)); @@ -134,7 +143,7 @@ private Reservation waitingReservation() { @Test void memberId로_예약_목록_조회_성공시_200을_반환한다() throws Exception { given(reservationService.findAll(any())) - .willReturn(new Reservations(List.of(approvedReservation()))); + .willReturn(new Reservations(List.of(approvedReservationEntity()))); mockMvc.perform(get("/reservations").param("memberId", "1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.reservations.length()").value(1)) diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index c9ec6a2cde..9eddd93126 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -15,6 +15,7 @@ import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; +import roomescape.domain.reservation.ReservationWithRank; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; @@ -269,16 +270,15 @@ private void givenNow(LocalDateTime dateTime) { @Test void 단건_조회시_존재하는_ID면_결과를_반환한다() { - given(reservationRepository.getById(EXISTS_ID)).willReturn(DUMMY); - given(reservationRepository.findBySlot_Id(EXISTS_ID)).willReturn(List.of(DUMMY)); - Reservation result = reservationService.find(EXISTS_ID); + given(reservationRepository.getByIdWithRank(EXISTS_ID)).willReturn(new ReservationWithRank(DUMMY, 0L)); + ReservationWithRank result = reservationService.find(EXISTS_ID); - Assertions.assertThat(result.getId()).isEqualTo(EXISTS_ID); + Assertions.assertThat(result.reservation().getId()).isEqualTo(EXISTS_ID); } @Test void 단건_조회시_존재하지_않는_ID면_예외가_발생한다() { - given(reservationRepository.getById(NOT_EXISTS_ID)).willThrow(new RoomEscapeException(DomainErrorCode.RESOURCE_NOT_FOUND, "test")); + given(reservationRepository.getByIdWithRank(NOT_EXISTS_ID)).willThrow(new RoomEscapeException(DomainErrorCode.RESOURCE_NOT_FOUND, "test")); Assertions.assertThatThrownBy(() -> reservationService.find(NOT_EXISTS_ID)) .isInstanceOf(RoomEscapeException.class); } @@ -306,10 +306,9 @@ private void givenNow(LocalDateTime dateTime) { @Test void 첫번째_예약은_승인_상태이다() { - given(reservationRepository.getById(EXISTS_ID)).willReturn(DUMMY); - given(reservationRepository.findBySlot_Id(EXISTS_ID)).willReturn(List.of(DUMMY)); - Reservation result = reservationService.find(EXISTS_ID); + given(reservationRepository.getByIdWithRank(EXISTS_ID)).willReturn(new ReservationWithRank(DUMMY, 0L)); + ReservationWithRank result = reservationService.find(EXISTS_ID); - Assertions.assertThat(result.getStatus()).isEqualTo(Status.APPROVED); + Assertions.assertThat(result.reservation().getStatus()).isEqualTo(Status.APPROVED); } } From bb681fbf59f6831810011db1c62766d0a2a1c620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=88=98=ED=98=84?= Date: Mon, 22 Jun 2026 14:24:17 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[4=EB=8B=A8=EA=B3=84]=20flush=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=ED=95=99=EC=8A=B5,=20jpa=20=EB=8F=99=EC=8B=9C?= =?UTF-8?q?=EC=84=B1=20=EC=BD=94=EB=93=9C=20=ED=95=99=EC=8A=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/SlotRepository.java | 16 ++- .../service/ReservationService.java | 18 ++- .../roomescape/FlushOrderObservation.java | 111 ++++++++++++++++++ .../repository/SlotRepositoryTest.java | 17 --- 4 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 src/test/java/roomescape/FlushOrderObservation.java diff --git a/src/main/java/roomescape/domain/reservation/SlotRepository.java b/src/main/java/roomescape/domain/reservation/SlotRepository.java index 491331bc05..a712b416d4 100644 --- a/src/main/java/roomescape/domain/reservation/SlotRepository.java +++ b/src/main/java/roomescape/domain/reservation/SlotRepository.java @@ -1,6 +1,8 @@ package roomescape.domain.reservation; +import jakarta.persistence.LockModeType; 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; import roomescape.domain.RoomEscapeException; @@ -17,18 +19,24 @@ public interface SlotRepository extends JpaRepository { List findByDateAndThemeId(ReservationDate date, Long themeId); - @Query("SELECT r.slot FROM Reservation r WHERE r.member.id = :memberId") - List findAllByMemberId(@Param("memberId") Long memberId); - - boolean existsByTimeId(Long timeId); +boolean existsByTimeId(Long timeId); boolean existsByThemeId(Long themeId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT s FROM Slot s WHERE s.id = :id") + Optional findByIdForUpdate(@Param("id") Long id); + default Slot getById(Long id) { return findById(id) .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 예약 슬롯을 찾을 수 없습니다. : " + id)); } + default Slot getByIdForUpdate(Long id) { + return findByIdForUpdate(id) + .orElseThrow(() -> new RoomEscapeException(RESOURCE_NOT_FOUND, "해당 예약 슬롯을 찾을 수 없습니다. : " + id)); + } + default Slot findOrCreate(ReservationDate date, ReservationTime time, Theme theme, LocalDateTime now) { return findByDateAndTimeAndTheme(date, time, theme) .orElseGet(() -> save(Slot.create(date, time, theme, now))); diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 603fe4bac7..18fdb0d354 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -9,6 +9,7 @@ import roomescape.domain.reservation.ReservationWithRank; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; +import roomescape.domain.reservation.SlotRepository; import roomescape.domain.reservation.Status; import java.time.Clock; @@ -21,24 +22,27 @@ public class ReservationService { private final Clock clock; private final ReservationAssembler assembler; private final ReservationRepository reservationRepository; + private final SlotRepository slotRepository; private final MemberRepository memberRepository; public ReservationService( Clock clock, ReservationAssembler assembler, ReservationRepository reservationRepository, + SlotRepository slotRepository, MemberRepository memberRepository ) { this.clock = clock; this.assembler = assembler; this.reservationRepository = reservationRepository; + this.slotRepository = slotRepository; this.memberRepository = memberRepository; } @Transactional public Reservation reserve(ReservationCreateCommand command) { Reservation assembled = assembler.from(command); - Slot slot = assembled.getSlot(); + Slot slot = slotRepository.getByIdForUpdate(assembled.getSlot().getId()); Reservations existing = new Reservations(reservationRepository.findBySlot_Id(slot.getId())); Reservation joined = existing.join(assembled); @@ -67,8 +71,14 @@ public ReservationWithRank update(ReservationUpdateCommand command, long id) { Reservation existing = reservationRepository.getById(id); Reservation assembled = assembler.from(command); - Slot newSlot = assembled.getSlot(); + Long newSlotId = assembled.getSlot().getId(); Long oldSlotId = existing.getSlotId(); + slotRepository.getByIdForUpdate(newSlotId); + if (!oldSlotId.equals(newSlotId)) { + slotRepository.getByIdForUpdate(oldSlotId); + } + + Slot newSlot = assembled.getSlot(); Reservations slotReservations = new Reservations(reservationRepository.findBySlot_Id(newSlot.getId())).excluding(id); Reservation template = slotReservations.join(assembled); @@ -82,12 +92,14 @@ public ReservationWithRank update(ReservationUpdateCommand command, long id) { promoteFirstWaiting(oldSlotId); } - return find(id); + long rank = reservationRepository.findRankById(id); + return new ReservationWithRank(existing, rank); } @Transactional public void cancel(long reservationId, Long memberId) { Reservation reservation = reservationRepository.getById(reservationId); + slotRepository.getByIdForUpdate(reservation.getSlotId()); LocalDateTime now = LocalDateTime.now(clock); reservation.validateCancellable(now); diff --git a/src/test/java/roomescape/FlushOrderObservation.java b/src/test/java/roomescape/FlushOrderObservation.java new file mode 100644 index 0000000000..66c0011361 --- /dev/null +++ b/src/test/java/roomescape/FlushOrderObservation.java @@ -0,0 +1,111 @@ +package roomescape; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +/** + * flush 순서 관찰 테스트 + * + * 목적: Hibernate가 변경을 flush할 때 "호출 순서"가 아니라 + * "작업 타입별 정해진 순서"(INSERT → UPDATE → DELETE)로 내보냄을 관찰한다. + * + * 관찰 방법: System.out 마킹과 Hibernate SQL 로그 순서를 대조. + * remove()를 persist()보다 먼저 호출해도, 로그엔 INSERT가 DELETE보다 먼저 찍힌다. + * + * 주의: 실제 도메인(status 방식)은 자동 승인이 UPDATE 하나뿐이라 + * 이 현상이 발생하지 않는다. 그래서 관찰 전용 임시 엔티티로 재현한다. + */ + +@DataJpaTest +public class FlushOrderObservation { + + @Autowired + private EntityManager em; + + @Entity(name = "FlushItem") + @SequenceGenerator(name = "flush_seq", sequenceName = "flush_seq", allocationSize = 1) + static class FlushItem { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "flush_seq") + Long id; + String name; + + FlushItem() { + } + + FlushItem(String name) { + this.name = name; + } + } + + // ================================================================ + // 관찰: remove를 먼저 호출해도 INSERT가 DELETE보다 먼저 flush + // ================================================================ + @Test + @DisplayName("flush 순서: 호출 순서와 무관하게 INSERT가 DELETE보다 먼저") + void insertBeforeDelete() { + // given: 미리 하나 저장, 컨텍스트 비우기 + FlushItem existing = new FlushItem("기존"); + em.persist(existing); + em.flush(); + em.clear(); + + FlushItem toDelete = em.find(FlushItem.class, existing.id); + + // when: remove를 persist보다 "먼저" 호출 + System.out.println(">>> remove 먼저 호출 (DELETE 큐에)"); + em.remove(toDelete); + + System.out.println(">>> persist 나중 호출 (SEQUENCE라 INSERT 지연됨 — 시퀀스 조회만)"); + em.persist(new FlushItem("신규")); + + System.out.println(">>> flush — 로그에 INSERT가 DELETE보다 먼저 찍히는지 확인"); + em.flush(); + System.out.println(">>> flush 끝"); + + // 예측: 호출은 remove → persist 순이었지만 + // flush 로그는 INSERT(신규) → DELETE(기존) 순으로 나감 + // 이유: Hibernate는 타입별 정해진 순서(INSERT→UPDATE→DELETE)로 flush + // + // ※ 만약 IDENTITY였다면: persist 때 INSERT가 이미 나가서 + // flush 땐 DELETE만 보임 → 이 재배열 현상을 관찰 못 함 + } + + // ================================================================ + // 순서를 강제로 바꾸려면 — 중간 flush + // ================================================================ + @Test + @DisplayName("순서 강제: 중간에 flush()를 끼우면 DELETE를 먼저 내보낼 수 있다") + void forceDeleteFirst() { + FlushItem existing = new FlushItem("기존"); + em.persist(existing); + em.flush(); + em.clear(); + + FlushItem toDelete = em.find(FlushItem.class, existing.id); + + System.out.println(">>> remove 호출"); + em.remove(toDelete); + + System.out.println(">>> 중간 flush — 여기서 DELETE를 강제로 먼저 내보냄"); + em.flush(); // remove까지의 변경을 먼저 내보냄 → DELETE 발행 + + System.out.println(">>> persist 호출"); + em.persist(new FlushItem("신규")); + + System.out.println(">>> 최종 flush — 이제 INSERT 발행"); + em.flush(); + System.out.println(">>> 끝"); + + // 예측: 중간 flush 덕에 DELETE → INSERT 순으로 나감 + // 활용: 유니크 제약 충돌을 피하려 "지우고 만들기" 순서가 필요할 때 + } +} diff --git a/src/test/java/roomescape/repository/SlotRepositoryTest.java b/src/test/java/roomescape/repository/SlotRepositoryTest.java index 44b9af3f15..97f2aeb381 100644 --- a/src/test/java/roomescape/repository/SlotRepositoryTest.java +++ b/src/test/java/roomescape/repository/SlotRepositoryTest.java @@ -191,21 +191,4 @@ void existsByThemeId_false() { assertThat(slotRepository.existsByThemeId(theme.getId())).isFalse(); } - - @Test - @DisplayName("회원ID로 슬롯 조회") - void findAllByMemberId() { - ReservationTime time = givenTime(14); - Theme theme = givenTheme("테스트 테마"); - Slot slot = givenSlot(new ReservationDate(TODAY), time, theme); - Member member1 = givenMember("유저1"); - Member member2 = givenMember("유저2"); - reservationRepository.save(Reservation.create(member1, slot).withStatus(Status.APPROVED)); - reservationRepository.save(Reservation.create(member2, slot).withStatus(Status.WAITING)); - - List slots = slotRepository.findAllByMemberId(member1.getId()); - - assertThat(slots).hasSize(1); - assertThat(slots.get(0).getId()).isEqualTo(slot.getId()); - } }