From 42eabb94791571003bea2601c922d570f968573a Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Tue, 2 Jun 2026 17:06:46 +0900 Subject: [PATCH 01/26] =?UTF-8?q?docs:=20README.md=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1339319e0d..f31e257e05 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ docker-compose up - [x] 같은 슬롯에 대한 대기는 신청 순서대로 순번이 부여된다. - [x] 같은 사용자가 같은 슬롯에 중복 대기할 수 없다. - [x] 사용자는 본인의 대기를 취소할 수 있다. +- [ ] 예약 취소 시 대기 중인 예약이 자동으로 승인으로 변경된다. +- [ ] 대기가 예약으로 전환되면 해당 슬롯의 나머지 대기 순번이 재정렬된다. +- [ ] 예약이 취소되면 해당 슬롯의 대기 순번이 재정렬된다. ### 예약 조회 @@ -75,18 +78,18 @@ docker-compose up ### 사용자 API -| 기능 | 메서드 / URL | 요청 본문 | 쿼리 파라미터 | 응답 | -|----------|-----------------------------|-----------------------------------------------------|----------------------------------------------------|----------------------------------------------------------------------------------------| -| 예약 목록 조회 | `GET /reservations` | - | reservationName - optional | `200 OK`
`[{reservationId, reservationName, date, theme, time}, ...]` | -| 예약 단건 조회 | `GET /reservations/{id}` | - | - | `200 OK`
`{reservationId, reservationName, date, theme, time}` | -| 예약 추가 | `POST /reservations` | `{reservationName, themeId, date, timeId}` | - | `201 Created`
`{reservationId, reservationName, theme: {...}, date, time: {...}}` | -| 예약 취소 | `DELETE /reservations/{id}` | - | - | `200 OK`
`{reservationId, reservationName, theme: {...}, date, time: {...}}` | -| 예약 변경 | `PUT /reservations/{id}` | `{reservationName, theme: {...}, date, time: {...}` | - | `200 OK`
`{reservationId, reservationName, theme: {...}, date, time: {...}}` | -| 시간 조회 | `GET /times` | - | - | `200 OK`
`[{timeId, startAt}, ...]` | -| 시간 조회 | `GET /times/available` | - | `date`, `themeId` | `200 OK`
`[{timeId, startAt}, ...]` | -| 테마 목록 조회 | `GET /themes` | - | - | `200 OK`
`[{themeId, reservationName, description, url}, ...]` | -| 테마 단건 조회 | `GET /themes/{id}` | - | - | `200 OK`
`{themeId, reservationName, description, url}` | -| 인기 테마 조회 | GET /themes/famous | - | days - optional, date - optional, limit - optional | `200 OK`
`[{themeId, reservationName, description, url}, ...]` | +| 기능 | 메서드 / URL | 요청 본문 | 쿼리 파라미터 | 응답 | +|----------|-----------------------------|-----------------------------------------------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------| +| 예약 목록 조회 | `GET /reservations` | - | reservationName - optional | `200 OK`
`[{reservationId, reservationName, date, theme, time}, ...]` | +| 예약 단건 조회 | `GET /reservations/{id}` | - | - | `200 OK`
`{reservationId, reservationName, date, theme, time}` | +| 예약 추가 | `POST /reservations` | `{reservationName, themeId, date, timeId}` | - | `201 Created`
`{reservationId, reservationName, theme: {...}, date, time: {...}}` | +| 예약 취소 | `DELETE /reservations/{id}` | - | - | `200 OK`
`{reservationId, reservationName, theme: {...}, date, time: {...}}` | +| 예약 변경 | `PUT /reservations/{id}` | `{reservationName, theme: {...}, date, time: {...}` | - | `200 OK`
`{reservationId, reservationName, theme: {...}, date, time: {...}}` | +| 시간 조회 | `GET /times` | - | - | `200 OK`
`[{timeId, startAt}, ...]` | +| 시간 조회 | `GET /times/available` | - | `date`, `themeId` | `200 OK`
`[{timeId, startAt}, ...]` | +| 테마 목록 조회 | `GET /themes` | - | - | `200 OK`
`[{themeId, reservationName, description, url}, ...]` | +| 테마 단건 조회 | `GET /themes/{id}` | - | - | `200 OK`
`{themeId, reservationName, description, url}` | +| 인기 테마 조회 | GET /themes/famous | - | recentDays - optional, baseDate - optional, limit - optional | `200 OK`
`[{themeId, reservationName, description, url}, ...]` | ### 어드민 API @@ -101,10 +104,12 @@ docker-compose up ### 상태 코드 분류 기준 -| 상태 코드 | 의미 | 사용 예시 | -|-------------------|-------------------|------------------| -| `400 Bad Request` | 요청 형식 또는 필수값이 잘못됨 | 필수 필드 누락, 타입 불일치 | -| `404 Not Found` | 요청한 리소스가 존재하지 않음 | 존재하지 않는 예약 ID 조회 | +| 상태 코드 | 의미 | 사용 예시 | +|----------------------------|-------------------|-----------------------| +| `400 Bad Request` | 요청 형식 또는 필수값이 잘못됨 | 필수 필드 누락, 타입 불일치 | +| `404 Not Found` | 요청한 리소스가 존재하지 않음 | 존재하지 않는 예약 ID 조회 | +| `409 Conflict` | DB 제약조건 위반 | 중복 예약, 참조되고 있는 리소스 삭제 | +| `422 Unprocessable Entity` | 비즈니스 규칙 위반 | 과거 날짜 예약, 운영 시간 외 예약 | ### 응답 본문 형식 From 9b10514cd69868cdb36d3c1d4e65892d022c4e78 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Wed, 3 Jun 2026 12:41:29 +0900 Subject: [PATCH 02/26] =?UTF-8?q?refactor:=20ReservationName,=20Reservatio?= =?UTF-8?q?nTime,=20Theme=EC=9D=84=20Slot=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../roomescape/domain/reservation/Rank.java | 2 +- .../domain/reservation/Reservation.java | 45 +++--- .../domain/reservation/Reservations.java | 2 +- .../roomescape/domain/reservation/Slot.java | 46 ++++++ .../repository/RepositoryRowMapper.java | 50 +++++++ .../repository/ReservationRepository.java | 88 +++++------- .../repository/ReservationTimeRepository.java | 7 +- .../roomescape/repository/SlotRepository.java | 62 ++++++++ .../repository/ThemeRepository.java | 11 +- .../service/ReservationService.java | 28 ++-- src/main/resources/data.sql | 114 +++++++++------ src/main/resources/schema.sql | 39 +++-- .../java/roomescape/MissionStep2Test.java | 6 +- .../java/roomescape/RoomEscapeFixture.java | 14 +- .../roomescape/RoomescapeApplicationTest.java | 8 +- .../domain/reservation/ReservationTest.java | 18 +-- .../domain/reservation/SlotTest.java | 31 ++++ .../repository/ReservationRepositoryTest.java | 16 ++- .../repository/SlotRepositoryTest.java | 135 ++++++++++++++++++ .../service/ReservationServiceTest.java | 63 ++++---- 21 files changed, 574 insertions(+), 213 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservation/Slot.java create mode 100644 src/main/java/roomescape/repository/RepositoryRowMapper.java create mode 100644 src/main/java/roomescape/repository/SlotRepository.java create mode 100644 src/test/java/roomescape/domain/reservation/SlotTest.java create mode 100644 src/test/java/roomescape/repository/SlotRepositoryTest.java diff --git a/.gitignore b/.gitignore index c2065bc262..e741cd6a7e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +.claude diff --git a/src/main/java/roomescape/domain/reservation/Rank.java b/src/main/java/roomescape/domain/reservation/Rank.java index 60e272e266..4caea99698 100644 --- a/src/main/java/roomescape/domain/reservation/Rank.java +++ b/src/main/java/roomescape/domain/reservation/Rank.java @@ -4,7 +4,7 @@ import common.exception.RoomEscapeException; public class Rank { - private static final int MIN_RANK_VALUE = 1; + private static final int MIN_RANK_VALUE = 0; private final int value; diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 83b06043cd..c2461d16e5 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -9,44 +9,32 @@ public class Reservation { private final long id; private final ReservationName name; - private final ReservationDate date; - private final ReservationTime time; - private final Theme theme; + private final Slot slot; private final Status status; private final LocalDateTime createdAt; - private Reservation(long id, ReservationName name, ReservationDate date, ReservationTime time, - Theme theme, Status status, LocalDateTime createdAt) { + private Reservation(long id, ReservationName name, Slot slot, Status status, LocalDateTime createdAt) { this.id = id; this.name = Objects.requireNonNull(name); - this.date = Objects.requireNonNull(date); - this.time = Objects.requireNonNull(time); - this.theme = Objects.requireNonNull(theme); + this.slot = Objects.requireNonNull(slot); this.status = Objects.requireNonNull(status); this.createdAt = Objects.requireNonNull(createdAt); } - public static Reservation load(long id, ReservationName reservationName, ReservationDate date, ReservationTime time, - Theme theme, Status status, LocalDateTime dateTime) { - return new Reservation(id, reservationName, date, time, theme, status, dateTime); + public static Reservation load(long id, ReservationName reservationName, Slot slot, Status status, + LocalDateTime createdAt) { + return new Reservation(id, reservationName, slot, status, createdAt); } - public static Reservation reserve( - ReservationName reservationName, - ReservationDate date, - ReservationTime time, - Theme theme, - Status status, - LocalDateTime now - ) { + public static Reservation reserve(ReservationName reservationName, Slot slot, Status status, LocalDateTime now) { Objects.requireNonNull(now); - Reservation reservation = new Reservation(0L, reservationName, date, time, theme, status, now); + Reservation reservation = new Reservation(0L, reservationName, slot, status, now); reservation.ensureNotPast(now); return reservation; } public void ensureNotPast(LocalDateTime now) { - LocalDateTime requestDateTime = LocalDateTime.of(date.getValue(), time.getStartAt()); + LocalDateTime requestDateTime = LocalDateTime.of(slot.getDate().getValue(), slot.getTime().getStartAt()); if (requestDateTime.isBefore(now)) { throw new RoomEscapeException(ErrorCode.PAST_RESERVATION_NOT_ALLOWED); @@ -61,16 +49,20 @@ public ReservationName getName() { return name; } + public Slot getSlot() { + return slot; + } + public ReservationDate getDate() { - return date; + return slot.getDate(); } public ReservationTime getTime() { - return time; + return slot.getTime(); } public Theme getTheme() { - return theme; + return slot.getTheme(); } public Status getStatus() { @@ -87,12 +79,11 @@ public boolean equals(Object o) { return false; } Reservation that = (Reservation) o; - return id == that.id && Objects.equals(name, that.name) && Objects.equals(date, that.date) - && Objects.equals(time, that.time) && Objects.equals(theme, that.theme); + return id == that.id && Objects.equals(name, that.name) && Objects.equals(slot, that.slot); } @Override public int hashCode() { - return Objects.hash(id, name, date, time, theme); + return Objects.hash(id, name, slot); } } diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java index 2b856cd070..bd7886406b 100644 --- a/src/main/java/roomescape/domain/reservation/Reservations.java +++ b/src/main/java/roomescape/domain/reservation/Reservations.java @@ -13,7 +13,7 @@ public Rank rankOf(Reservation target) { long earlierCount = reservations.stream() .filter(r -> isEarlierThan(r, target)) .count(); - return new Rank((int) earlierCount + 1); + return new Rank((int) earlierCount); } private boolean isEarlierThan(Reservation source, Reservation target) { diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java new file mode 100644 index 0000000000..3b52eb683a --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -0,0 +1,46 @@ +package roomescape.domain.reservation; + +import java.util.Objects; +import roomescape.domain.theme.Theme; + +public class Slot { + private final long id; + private final ReservationDate date; + private final ReservationTime time; + private final Theme theme; + + private Slot(long id, ReservationDate date, ReservationTime time, Theme theme) { + this.id = id; + this.date = Objects.requireNonNull(date); + this.time = Objects.requireNonNull(time); + this.theme = Objects.requireNonNull(theme); + } + + public static Slot load(long id, ReservationDate date, ReservationTime time, Theme theme) { + return new Slot(id, date, time, theme); + } + + public static Slot create(ReservationDate date, ReservationTime time, Theme theme) { + return new Slot(0, date, time, theme); + } + + public Slot withId(long id) { + return new Slot(id, date, time, theme); + } + + public long getId() { + return id; + } + + public ReservationDate getDate() { + return date; + } + + public ReservationTime getTime() { + return time; + } + + public Theme getTheme() { + return theme; + } +} diff --git a/src/main/java/roomescape/repository/RepositoryRowMapper.java b/src/main/java/roomescape/repository/RepositoryRowMapper.java new file mode 100644 index 0000000000..eebf9c42a1 --- /dev/null +++ b/src/main/java/roomescape/repository/RepositoryRowMapper.java @@ -0,0 +1,50 @@ +package roomescape.repository; + +import java.sql.ResultSet; +import java.sql.SQLException; +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; +import roomescape.domain.theme.Theme; +import roomescape.domain.theme.ThemeName; +import roomescape.domain.theme.ThumbnailUrl; + +public final class RepositoryRowMapper { + private RepositoryRowMapper() { + } + + public static Reservation reservationRowMapper(ResultSet rs) throws SQLException { + return Reservation.load( + rs.getLong("reservation_id"), + new ReservationName(rs.getString("name")), + slotRowMapper(rs), + Status.valueOf(rs.getString("status")), + rs.getTimestamp("created_at").toLocalDateTime() + ); + } + + public static Slot slotRowMapper(ResultSet rs) throws SQLException { + return Slot.load( + rs.getLong("slot_id"), + new ReservationDate(rs.getDate("slot_date").toLocalDate()), + reservationTimeRowMapper(rs), + themeRowMapper(rs) + ); + } + + public static ReservationTime reservationTimeRowMapper(ResultSet rs) throws SQLException { + return ReservationTime.of(rs.getLong("time_id"), rs.getTime("start_at").toLocalTime()); + } + + public static Theme themeRowMapper(ResultSet rs) throws SQLException { + return Theme.load( + rs.getLong("theme_id"), + new ThemeName(rs.getString("theme_name")), + rs.getString("description"), + new ThumbnailUrl(rs.getString("thumbnail_url")) + ); + } +} diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index 42e0084166..12c36efa77 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -10,48 +10,35 @@ import org.springframework.stereotype.Repository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; -import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Status; import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeName; -import roomescape.domain.theme.ThumbnailUrl; @Repository public class ReservationRepository { - public static final RowMapper RESERVATION_ROW_MAPPER = (resultSet, rowNum) -> Reservation.load( - resultSet.getLong("reservation_id"), - new ReservationName(resultSet.getString("name")), - new ReservationDate(resultSet.getDate("date").toLocalDate()), - ReservationTime.of(resultSet.getLong("time_id"), resultSet.getTime("start_at").toLocalTime()), - Theme.load(resultSet.getLong("theme_id"), new ThemeName(resultSet.getString("theme_name")), - resultSet.getString("description"), new ThumbnailUrl(resultSet.getString("thumbnail_url"))), - Status.valueOf(resultSet.getString("status")), - resultSet.getTimestamp("created_at").toLocalDateTime()); + public static final RowMapper RESERVATION_ROW_MAPPER = + (rs, rowNum) -> RepositoryRowMapper.reservationRowMapper(rs); private static final String SELECT_ALL = """ - SELECT r.id AS reservation_id, + SELECT r.id AS reservation_id, r.name, - r.date, r.status, r.created_at, - rt.id AS time_id, + s.id AS slot_id, + s.date AS slot_date, + rt.id AS time_id, rt.start_at, - t.id AS theme_id, - t.name AS theme_name, + t.id AS theme_id, + t.name AS theme_name, t.description, t.thumbnail_url FROM reservation r - INNER JOIN reservation_time rt ON r.time_id = rt.id - INNER JOIN theme t ON r.theme_id = t.id + INNER JOIN slot s ON r.slot_id = s.id + INNER JOIN reservation_time rt ON s.time_id = rt.id + INNER JOIN theme t ON s.theme_id = t.id """; private static final String UPDATE = """ UPDATE reservation - SET - name = ?, - date = ?, - time_id = ?, - theme_id = ?, - created_at = ? + SET name = ?, slot_id = ?, created_at = ? WHERE id = ? """; private static final String SELECT_BY_ID = SELECT_ALL + "WHERE r.id = ?"; @@ -59,19 +46,21 @@ public class ReservationRepository { private static final String EXISTS_BY_DATE_AND_TIME_AND_THEME_ID = """ SELECT EXISTS ( SELECT 1 - FROM reservation - WHERE date = ? AND time_id = ? AND theme_id = ? AND name = ? + FROM reservation r + INNER JOIN slot s ON r.slot_id = s.id + WHERE s.date = ? AND s.time_id = ? AND s.theme_id = ? AND r.name = ? ) """; private static final String EXISTS_APPROVED_BY_SLOT = """ SELECT EXISTS ( SELECT 1 - FROM reservation - WHERE date = ? AND time_id = ? AND theme_id = ? AND status = 'APPROVED' + FROM reservation r + INNER JOIN slot s ON r.slot_id = s.id + WHERE s.date = ? AND s.time_id = ? AND s.theme_id = ? AND r.status = 'APPROVED' ) """; private static final String SELECT_FIRST_WAITING_BY_SLOT = SELECT_ALL + """ - WHERE r.date = ? AND rt.id = ? AND t.id = ? AND r.status = 'WAITING' + WHERE s.date = ? AND rt.id = ? AND t.id = ? AND r.status = 'WAITING' ORDER BY r.created_at, r.id LIMIT 1 """; @@ -79,16 +68,18 @@ SELECT EXISTS ( private static final String EXISTS_BY_TIME_ID = """ SELECT EXISTS ( SELECT 1 - FROM reservation - WHERE time_id = ? - ) + FROM reservation r + INNER JOIN slot s ON r.slot_id = s.id + WHERE s.time_id = ? + ) """; private static final String EXISTS_BY_THEME_ID = """ SELECT EXISTS ( SELECT 1 - FROM reservation - WHERE theme_id = ? - ) + FROM reservation r + INNER JOIN slot s ON r.slot_id = s.id + WHERE s.theme_id = ? + ) """; private final JdbcTemplate jdbcTemplate; @@ -117,32 +108,27 @@ public Optional findById(long reservationId) { public Reservation save(Reservation reservation) { Map params = Map.of( "name", reservation.getName().getValue(), - "date", reservation.getDate().getValue(), - "time_id", reservation.getTime().getId(), - "theme_id", reservation.getTheme().getId(), + "slot_id", reservation.getSlot().getId(), "status", reservation.getStatus().name(), "created_at", reservation.getCreatedAt() ); long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return Reservation.load(generatedKey, - reservation.getName(), - reservation.getDate(), reservation.getTime(), - reservation.getTheme(), reservation.getStatus(), reservation.getCreatedAt()); + return Reservation.load(generatedKey, reservation.getName(), reservation.getSlot(), + reservation.getStatus(), reservation.getCreatedAt()); } public Reservation update(long id, Reservation target) { - jdbcTemplate.update(UPDATE, target.getName().getValue(), target.getDate().getValue(), target.getTime().getId(), - target.getTheme().getId(), target.getCreatedAt(), id); + jdbcTemplate.update(UPDATE, target.getName().getValue(), target.getSlot().getId(), + target.getCreatedAt(), id); - return Reservation.load(id, target.getName(), target.getDate(), target.getTime(), target.getTheme(), + return Reservation.load(id, target.getName(), target.getSlot(), target.getStatus(), target.getCreatedAt()); } public void deleteById(Long id) { - String sql = "delete from reservation where id = ?"; - jdbcTemplate.update(sql, id); + jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id); } public boolean existsByTimeId(long reservationTimeId) { @@ -157,8 +143,8 @@ public boolean existsByThemeId(long themeId) { public boolean existsByTimeAndThemeAndDateAndName(Long timeId, Long themeId, LocalDate date, String name) { return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(EXISTS_BY_DATE_AND_TIME_AND_THEME_ID, Boolean.class, date, timeId, - themeId, name)); + jdbcTemplate.queryForObject(EXISTS_BY_DATE_AND_TIME_AND_THEME_ID, Boolean.class, + date, timeId, themeId, name)); } public boolean existsApprovedByTimeAndThemeAndDate(Long timeId, Long themeId, LocalDate date) { @@ -177,7 +163,7 @@ public void updateStatus(Long id, Status status) { } public List findByTimeAndThemeAndDate(ReservationTime time, Theme theme, ReservationDate date) { - String sql = SELECT_ALL + "WHERE r.date = ? AND t.id = ? AND rt.id = ?"; + String sql = SELECT_ALL + "WHERE s.date = ? AND t.id = ? AND rt.id = ?"; return jdbcTemplate.query(sql, RESERVATION_ROW_MAPPER, date.getValue(), theme.getId(), time.getId()); } } diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java index 5daaaf279e..5bb1f042ff 100644 --- a/src/main/java/roomescape/repository/ReservationTimeRepository.java +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -58,9 +58,10 @@ public List findByDateAndTheme(LocalDate date, long themeId) { SELECT rt.id, rt.start_at FROM reservation_time AS rt WHERE rt.id NOT IN ( - SELECT r.time_id - FROM reservation AS r - WHERE r.date = ? AND r.theme_id = ? + SELECT s.time_id + FROM slot s + INNER JOIN reservation r ON r.slot_id = s.id + WHERE s.date = ? AND s.theme_id = ? ) """; return jdbcTemplate.query(sql, RESERVATION_TIME_ROW_MAPPER, date, themeId); diff --git a/src/main/java/roomescape/repository/SlotRepository.java b/src/main/java/roomescape/repository/SlotRepository.java new file mode 100644 index 0000000000..3c4bcbe044 --- /dev/null +++ b/src/main/java/roomescape/repository/SlotRepository.java @@ -0,0 +1,62 @@ +package roomescape.repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import roomescape.domain.reservation.Slot; + +@Repository +public class SlotRepository { + private static final String SELECT_ALL = """ + SELECT s.id AS slot_id, + s.date AS slot_date, + rt.id AS time_id, + rt.start_at, + t.id AS theme_id, + t.name AS theme_name, + t.description, + t.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 static final RowMapper SLOT_ROW_MAPPER = + (rs, rowNum) -> RepositoryRowMapper.slotRowMapper(rs); + + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert simpleJdbcInsert; + + public SlotRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("slot") + .usingGeneratedKeyColumns("id"); + } + + public Slot save(Slot slot) { + Map params = Map.of( + "date", slot.getDate().getValue(), + "time_id", slot.getTime().getId(), + "theme_id", slot.getTheme().getId() + ); + long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); + return slot.withId(generatedKey); + } + + public Optional findById(long id) { + String sql = SELECT_ALL + "WHERE s.id = ?"; + List result = jdbcTemplate.query(sql, SLOT_ROW_MAPPER, id); + return result.stream().findFirst(); + } + + public Optional findByDateAndTimeAndTheme(LocalDate date, long timeId, long themeId) { + String sql = SELECT_ALL + "WHERE s.date = ? AND s.time_id = ? AND s.theme_id = ?"; + List result = jdbcTemplate.query(sql, SLOT_ROW_MAPPER, date, timeId, themeId); + return result.stream().findFirst(); + } +} diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java index 5aec9be4b9..55987320d5 100644 --- a/src/main/java/roomescape/repository/ThemeRepository.java +++ b/src/main/java/roomescape/repository/ThemeRepository.java @@ -55,11 +55,12 @@ public List findFamous(FamousThemeCondition condition) { 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 ? AND ? - GROUP BY theme_id - ORDER BY count(theme_id) DESC, theme_id DESC + SELECT s.theme_id, count(s.theme_id) AS cnt + FROM reservation r + INNER JOIN slot s ON r.slot_id = s.id + WHERE s.date BETWEEN ? AND ? + GROUP BY s.theme_id + ORDER BY count(s.theme_id) DESC, s.theme_id DESC LIMIT ? ) AS topN ON t.id = topN.theme_id ORDER BY topN.cnt DESC, topN.theme_id DESC diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index c6820edb48..6440c23dbc 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -17,10 +17,12 @@ import roomescape.domain.reservation.ReservationResult; 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.repository.ReservationRepository; import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.SlotRepository; import roomescape.repository.ThemeRepository; @Service @@ -28,12 +30,15 @@ public class ReservationService { private final ReservationRepository reservationRepository; private final ReservationTimeRepository reservationTimeRepository; private final ThemeRepository themeRepository; + private final SlotRepository slotRepository; public ReservationService(ReservationRepository reservationRepository, - ReservationTimeRepository reservationTimeRepository, ThemeRepository themeRepository) { + ReservationTimeRepository reservationTimeRepository, ThemeRepository themeRepository, + SlotRepository slotRepository) { this.reservationRepository = reservationRepository; this.reservationTimeRepository = reservationTimeRepository; this.themeRepository = themeRepository; + this.slotRepository = slotRepository; } @Transactional @@ -43,10 +48,11 @@ public ReservationResult reserve(ReservationCreateRequest request, LocalDateTime Status status = determineStatus(request.getTimeId(), request.getThemeId(), request.getDate()); - Reservation reservation = Reservation.reserve(new ReservationName(request.getName()), - new ReservationDate(request.getDate()), reservationTime, theme, status, now); + Slot slot = findOrCreateSlot(new ReservationDate(request.getDate()), reservationTime, theme); + Reservation reservation = Reservation.reserve(new ReservationName(request.getName()), slot, status, now); - validateIsDuplicateReservation(request.getTimeId(), request.getThemeId(), request.getDate(), request.getName()); + validateIsDuplicateNameReservation(request.getTimeId(), request.getThemeId(), request.getDate(), + request.getName()); Reservation saved = reservationRepository.save(reservation); Reservations reservations = new Reservations(reservationRepository.findByTimeAndThemeAndDate( @@ -100,10 +106,11 @@ public ReservationResult update(ReservationUpdateRequest request, long id, Local ReservationDate reservationDate = new ReservationDate(request.getDate()); ReservationTime reservationTime = findReservationTimeByTimeId(request.getTimeId()); - validateIsDuplicateReservation(request.getTimeId(), request.getThemeId(), request.getDate(), request.getName()); + validateIsDuplicateNameReservation(request.getTimeId(), request.getThemeId(), request.getDate(), + request.getName()); - Reservation target = Reservation.reserve(reservation.getName(), reservationDate, reservationTime, - reservation.getTheme(), reservation.getStatus(), now); + Slot slot = findOrCreateSlot(reservationDate, reservationTime, reservation.getTheme()); + Reservation target = Reservation.reserve(reservation.getName(), slot, reservation.getStatus(), now); Reservation updated = reservationRepository.update(id, target); Reservations reservations = new Reservations(reservationRepository.findByTimeAndThemeAndDate( @@ -131,6 +138,11 @@ public void cancel(long reservationId, LocalDateTime now) { } } + private Slot findOrCreateSlot(ReservationDate date, ReservationTime time, Theme theme) { + return slotRepository.findByDateAndTimeAndTheme(date.getValue(), time.getId(), theme.getId()) + .orElseGet(() -> slotRepository.save(Slot.create(date, time, theme))); + } + private Status determineStatus(long timeId, long themeId, LocalDate date) { if (reservationRepository.existsApprovedByTimeAndThemeAndDate(timeId, themeId, date)) { return Status.WAITING; @@ -148,7 +160,7 @@ private Theme findThemeByThemeId(long themeId) { () -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); } - private void validateIsDuplicateReservation(long timeId, long themeId, LocalDate date, String name) { + private void validateIsDuplicateNameReservation(long timeId, long themeId, LocalDate date, String name) { if (reservationRepository.existsByTimeAndThemeAndDateAndName(timeId, themeId, date, name)) { throw new RoomEscapeException(ErrorCode.DUPLICATE_RESERVATION); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 0499bf3f40..e02893ba92 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -18,50 +18,80 @@ INSERT INTO THEME (name, description, thumbnail_url) VALUES ('마법 학교', ' INSERT INTO THEME (name, description, thumbnail_url) VALUES ('고대 유적', '고대 문명의 유적을 탐험하세요', 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbfoc4tfrkbUaKHBGhvdiTtoyzUmh3YNRsuw&s'); 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'); --- RESERVATION: 33개 (2026-05-23 ~ 2026-05-30, 오늘=2026-05-27 전후 혼재) --- created_at: 항상 해당 row의 date보다 과거. id 순으로 증가하는 상대 순서 보존. --- Theme 1 (공포의 저택): 10건 → 1위 -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('김철수', '2026-05-23', 1, 1, 'APPROVED', '2026-05-21 09:12:33'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('이영희', '2026-05-23', 2, 1, 'APPROVED', '2026-05-21 11:45:07'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('박민수', '2026-05-24', 3, 1, 'APPROVED', '2026-05-22 14:30:51'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('홍길동', '2026-05-24', 4, 1, 'APPROVED', '2026-05-22 18:05:22'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('정수진', '2026-05-25', 5, 1, 'APPROVED', '2026-05-23 21:40:18'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('한동훈', '2026-05-25', 6, 1, 'APPROVED', '2026-05-24 08:15:44'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('임채원', '2026-05-26', 7, 1, 'APPROVED', '2026-05-24 10:50:09'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('서태양', '2026-05-27', 8, 1, 'APPROVED', '2026-05-25 13:22:37'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('김철수', '2026-05-28', 9, 1, 'APPROVED', '2026-05-26 16:48:55'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('유민호', '2026-05-30', 10, 1, 'APPROVED', '2026-05-28 20:11:02'); +-- SLOT: 30개 (date + time_id + theme_id 고유 조합) +-- Theme 1 (공포의 저택): slots 1~10 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 1, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 2, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 3, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 4, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 5, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 6, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 7, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 8, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 9, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 10, 1); +-- Theme 2 (우주 탐험): slots 11~18 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 3, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 4, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 5, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 6, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 7, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 8, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 9, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 10, 2); +-- Theme 3 (마법 학교): slots 19~24 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 1, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 2, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 3, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 4, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 5, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 6, 3); +-- Theme 4 (고대 유적): slots 25~28 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 7, 4); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 8, 4); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 9, 4); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 10, 4); +-- Theme 5 (탐정 사무소): slots 29~30 +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 11, 5); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 1, 5); +-- RESERVATION: 33건 (slot_id 참조) +-- Theme 1 (공포의 저택): 10건 → 1위 +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 1, 'APPROVED', '2026-05-21 09:12:33'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('이영희', 2, 'APPROVED', '2026-05-21 11:45:07'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('박민수', 3, 'APPROVED', '2026-05-22 14:30:51'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 4, 'APPROVED', '2026-05-22 18:05:22'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('정수진', 5, 'APPROVED', '2026-05-23 21:40:18'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('한동훈', 6, 'APPROVED', '2026-05-24 08:15:44'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('임채원', 7, 'APPROVED', '2026-05-24 10:50:09'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 8, 'APPROVED', '2026-05-25 13:22:37'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 9, 'APPROVED', '2026-05-26 16:48:55'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('유민호', 10, 'APPROVED', '2026-05-28 20:11:02'); -- Theme 2 (우주 탐험): 8건 → 2위 -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('강민준', '2026-05-23', 3, 2, 'APPROVED', '2026-05-20 07:33:19'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('조현아', '2026-05-24', 4, 2, 'APPROVED', '2026-05-22 09:58:41'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('김철수', '2026-05-25', 5, 2, 'APPROVED', '2026-05-23 12:27:06'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('홍길동', '2026-05-26', 6, 2, 'APPROVED', '2026-05-24 15:44:50'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('황준혁', '2026-05-26', 7, 2, 'APPROVED', '2026-05-25 19:09:28'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('송미래', '2026-05-27', 8, 2, 'APPROVED', '2026-05-26 08:41:13'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('안태양', '2026-05-28', 9, 2, 'APPROVED', '2026-05-27 11:16:39'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('배소희', '2026-05-30', 10, 2, 'APPROVED', '2026-05-29 14:52:04'); - +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('강민준', 11, 'APPROVED', '2026-05-20 07:33:19'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('조현아', 12, 'APPROVED', '2026-05-22 09:58:41'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 13, 'APPROVED', '2026-05-23 12:27:06'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 14, 'APPROVED', '2026-05-24 15:44:50'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('황준혁', 15, 'APPROVED', '2026-05-25 19:09:28'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('송미래', 16, 'APPROVED', '2026-05-26 08:41:13'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('안태양', 17, 'APPROVED', '2026-05-27 11:16:39'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('배소희', 18, 'APPROVED', '2026-05-29 14:52:04'); -- Theme 3 (마법 학교): 6건 → 3위 -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('권지훈', '2026-05-24', 1, 3, 'APPROVED', '2026-05-22 17:30:47'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('홍길동', '2026-05-25', 2, 3, 'APPROVED', '2026-05-23 20:55:21'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('김철수', '2026-05-26', 3, 3, 'APPROVED', '2026-05-25 09:05:58'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('류지아', '2026-05-27', 4, 3, 'APPROVED', '2026-05-26 12:38:16'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('서태양', '2026-05-28', 5, 3, 'APPROVED', '2026-05-27 15:11:33'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('서태양', '2026-05-30', 6, 3, 'APPROVED', '2026-05-29 18:47:09'); - +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('권지훈', 19, 'APPROVED', '2026-05-22 17:30:47'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 20, 'APPROVED', '2026-05-23 20:55:21'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 21, 'APPROVED', '2026-05-25 09:05:58'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('류지아', 22, 'APPROVED', '2026-05-26 12:38:16'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 23, 'APPROVED', '2026-05-27 15:11:33'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 24, 'APPROVED', '2026-05-29 18:47:09'); -- Theme 4 (고대 유적): 4건 → 4위 -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('홍길동', '2026-05-25', 7, 4, 'APPROVED', '2026-05-23 08:23:42'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('전현무', '2026-05-26', 8, 4, 'APPROVED', '2026-05-25 10:59:27'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('서태양', '2026-05-27', 9, 4, 'APPROVED', '2026-05-26 13:34:50'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('표민혁', '2026-05-28', 10, 4, 'APPROVED', '2026-05-27 16:20:15'); - +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 25, 'APPROVED', '2026-05-23 08:23:42'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('전현무', 26, 'APPROVED', '2026-05-25 10:59:27'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 27, 'APPROVED', '2026-05-26 13:34:50'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('표민혁', 28, 'APPROVED', '2026-05-27 16:20:15'); -- Theme 5 (탐정 사무소): 2건 → 5위 -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('서태양', '2026-05-26', 11, 5, 'APPROVED', '2026-05-24 19:48:33'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('홍길동', '2026-05-30', 1, 5, 'APPROVED', '2026-05-28 09:14:06'); - --- 같은 슬롯 예약 (대기 순번 테스트용): 2026-05-23, time_id=1, theme_id=1 --- 김철수(id=1)가 APPROVED, 이후 예약은 WAITING -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('대기자A', '2026-05-23', 1, 1, 'WAITING', '2026-05-21 09:30:00'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('대기자B', '2026-05-23', 1, 1, 'WAITING', '2026-05-21 10:00:00'); -INSERT INTO RESERVATION (name, date, time_id, theme_id, status, created_at) VALUES ('대기자C', '2026-05-23', 1, 1, 'WAITING', '2026-05-21 10:30:00'); \ No newline at end of file +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 29, 'APPROVED', '2026-05-24 19:48:33'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 30, 'APPROVED', '2026-05-28 09:14:06'); +-- 같은 슬롯 대기 예약 (slot 1 공유) +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자A', 1, 'WAITING', '2026-05-21 09:30:00'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자B', 1, 'WAITING', '2026-05-21 10:00:00'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자C', 1, 'WAITING', '2026-05-21 10:30:00'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index a44f2d08a7..6084bb2b8e 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,30 +1,39 @@ 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, + 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, + id BIGINT NOT NULL AUTO_INCREMENT, + start_at TIME NOT NULL, PRIMARY KEY (id) ); -CREATE TABLE reservation ( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(20) NOT NULL, - date DATE NOT NULL, - time_id BIGINT NOT NULL, +CREATE TABLE slot ( + id BIGINT NOT NULL AUTO_INCREMENT, + date DATE NOT NULL, + time_id BIGINT NOT NULL, theme_id BIGINT NOT NULL, - status VARCHAR(10) NOT NULL DEFAULT 'APPROVED', - created_at TIMESTAMP default CURRENT_TIMESTAMP, PRIMARY KEY (id), - FOREIGN KEY (time_id) REFERENCES reservation_time (id) ON DELETE RESTRICT, - FOREIGN KEY (theme_id) REFERENCES theme (id) ON DELETE RESTRICT + CONSTRAINT uk_slot UNIQUE (date, time_id, theme_id), + FOREIGN KEY (time_id) REFERENCES reservation_time (id) ON DELETE RESTRICT, + FOREIGN KEY (theme_id) REFERENCES theme (id) ON DELETE RESTRICT +); + +CREATE TABLE reservation ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(20) NOT NULL, + slot_id BIGINT 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) ON DELETE RESTRICT ); \ No newline at end of file diff --git a/src/test/java/roomescape/MissionStep2Test.java b/src/test/java/roomescape/MissionStep2Test.java index 34a9f54f08..ed3e9ce396 100644 --- a/src/test/java/roomescape/MissionStep2Test.java +++ b/src/test/java/roomescape/MissionStep2Test.java @@ -55,9 +55,9 @@ void init() { @Test void DB_조회_API_전환() { - jdbcTemplate.update("INSERT INTO reservation (name, date, time_id, theme_id) VALUES (?, ?, ?, ?)", "브라운", - "2023-08-05", - 1, 1); + jdbcTemplate.update("INSERT INTO slot (date, time_id, theme_id) VALUES (?, ?, ?)", + "2023-08-05", 1, 1); + jdbcTemplate.update("INSERT INTO reservation (name, slot_id) VALUES (?, ?)", "브라운", 1); List reservations = RestAssured.given().log().all() .when().get("/reservations") diff --git a/src/test/java/roomescape/RoomEscapeFixture.java b/src/test/java/roomescape/RoomEscapeFixture.java index ea348ec858..e30963a293 100644 --- a/src/test/java/roomescape/RoomEscapeFixture.java +++ b/src/test/java/roomescape/RoomEscapeFixture.java @@ -15,6 +15,7 @@ import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationResult; import roomescape.domain.reservation.ReservationTime; +import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; import roomescape.domain.theme.Theme; import roomescape.domain.theme.ThemeName; @@ -30,6 +31,7 @@ public class RoomEscapeFixture { private static final ReservationDate PAST_DATE = new ReservationDate(LocalDate.of(2000, 11, 11)); private static final ReservationTime TIME = ReservationTime.of(LocalTime.of(10, 0)); private static final Theme THEME = Theme.create(new ThemeName("공포"), "무서워요", new ThumbnailUrl("https://zeze.com")); + private static final Slot SLOT = Slot.create(FUTURE_DATE, TIME, THEME); private static final Rank APPROVE_RANK = new Rank(1); private static final Rank WAITING_RANK = new Rank(2); @@ -49,12 +51,16 @@ public static ReservationTime reservationTime() { return TIME; } + public static Slot slot() { + return SLOT; + } + public static Reservation reservationWithApproved() { - return Reservation.load(1L, NAME, FUTURE_DATE, TIME, THEME, Status.APPROVED, LocalDateTime.now(FIXED_CLOCK)); + return Reservation.load(1L, NAME, SLOT, Status.APPROVED, LocalDateTime.now(FIXED_CLOCK)); } public static Reservation reservationWithWaiting() { - return Reservation.load(2L, NAME, FUTURE_DATE, TIME, THEME, Status.WAITING, LocalDateTime.now(FIXED_CLOCK)); + return Reservation.load(2L, NAME, SLOT, Status.WAITING, LocalDateTime.now(FIXED_CLOCK)); } public static ReservationResult reservationResultWithApproved() { @@ -73,6 +79,10 @@ public static ReservationCreateRequest reservationCreateRequest() { return new ReservationCreateRequest(NAME.getValue(), FUTURE_DATE.getValue(), 1L, 1L); } + public static ReservationCreateRequest reservationCreateRequestWithName(ReservationName name) { + return new ReservationCreateRequest(name.getValue(), FUTURE_DATE.getValue(), 1L, 1L); + } + public static ReservationCreateRequest reservationCreateRequestWithNullName() { return new ReservationCreateRequest(null, FUTURE_DATE.getValue(), 1L, 1L); } diff --git a/src/test/java/roomescape/RoomescapeApplicationTest.java b/src/test/java/roomescape/RoomescapeApplicationTest.java index d4abf29314..8023fd34d3 100644 --- a/src/test/java/roomescape/RoomescapeApplicationTest.java +++ b/src/test/java/roomescape/RoomescapeApplicationTest.java @@ -22,17 +22,13 @@ class RoomescapeApplicationTest { private static final String AVAILABLE_DATE = "2099-06-01"; @Autowired private JdbcTemplate jdbcTemplate; - + @LocalServerPort int port; - @BeforeEach - void setUp() { - RestAssured.port = port; - } - @BeforeEach void init() { + RestAssured.port = port; jdbcTemplate.update("insert into reservation_time(start_at) values ('10:00')"); jdbcTemplate.update( "insert into theme(name, description, thumbnail_url) values ('공포', '무서워요', 'https://zeze.com')"); diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 5d4d2b30b1..1a66273df1 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -8,29 +8,23 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import roomescape.RoomEscapeFixture; -import roomescape.domain.theme.Theme; public class ReservationTest { @ParameterizedTest @MethodSource("nullCases") - void 매개변수에_NULL이_포함되면_예외가_발생한다(ReservationName reservationName, ReservationDate date, ReservationTime time, - Theme theme, Status status) { - assertThatThrownBy(() -> Reservation.reserve(reservationName, date, time, theme, status, LocalDateTime.MIN)) + void 매개변수에_NULL이_포함되면_예외가_발생한다(ReservationName reservationName, Slot slot, Status status) { + assertThatThrownBy(() -> Reservation.reserve(reservationName, slot, status, LocalDateTime.MIN)) .isInstanceOf(NullPointerException.class); } static Stream nullCases() { ReservationName name = RoomEscapeFixture.reservationName(); - ReservationDate date = RoomEscapeFixture.reservationDate(); - ReservationTime time = RoomEscapeFixture.reservationTime(); - Theme theme = RoomEscapeFixture.theme(); + Slot slot = RoomEscapeFixture.slot(); return Stream.of( - Arguments.of(null, date, time, theme, Status.APPROVED), - Arguments.of(name, null, time, theme, Status.APPROVED), - Arguments.of(name, date, null, theme, Status.APPROVED), - Arguments.of(name, date, time, null, Status.APPROVED), - Arguments.of(name, date, time, theme, null) + Arguments.of(null, slot, Status.APPROVED), + Arguments.of(name, null, Status.APPROVED), + Arguments.of(name, slot, null) ); } } diff --git a/src/test/java/roomescape/domain/reservation/SlotTest.java b/src/test/java/roomescape/domain/reservation/SlotTest.java new file mode 100644 index 0000000000..f6a22ac54a --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/SlotTest.java @@ -0,0 +1,31 @@ +package roomescape.domain.reservation; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import roomescape.RoomEscapeFixture; +import roomescape.domain.theme.Theme; + +public class SlotTest { + @ParameterizedTest + @MethodSource("nullCases") + void 매개변수에_NULL이_포함되면_예외가_발생한다(ReservationDate date, ReservationTime time, Theme theme) { + assertThatThrownBy(() -> Slot.create(date, time, theme)) + .isInstanceOf(NullPointerException.class); + } + + static Stream nullCases() { + ReservationDate date = RoomEscapeFixture.reservationDate(); + ReservationTime time = RoomEscapeFixture.reservationTime(); + Theme theme = RoomEscapeFixture.theme(); + + return Stream.of( + Arguments.of(null, time, theme), + Arguments.of(date, null, theme), + Arguments.of(date, time, null) + ); + } +} diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index e05cf71ed3..0e0ad7df30 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -19,6 +19,7 @@ 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; import roomescape.domain.theme.Theme; import roomescape.domain.theme.ThemeName; @@ -29,7 +30,8 @@ @Import(value = { ReservationRepository.class, ReservationTimeRepository.class, - ThemeRepository.class + ThemeRepository.class, + SlotRepository.class }) class ReservationRepositoryTest { private final static Clock FIXED_CLOCK = Clock.fixed( @@ -49,6 +51,9 @@ class ReservationRepositoryTest { @Autowired private ThemeRepository themeRepository; + @Autowired + private SlotRepository slotRepository; + private ReservationTime giveTime(int hour) { return timeRepository.save(ReservationTime.of(LocalTime.of(hour, 0))); } @@ -58,14 +63,19 @@ private Theme giveTheme(String name) { Theme.create(new ThemeName(name), name + "테마에 관한 설명 입니다.", new ThumbnailUrl("https://test-theme.com"))); } + private Slot giveSlot(LocalDate date, ReservationTime time, Theme theme) { + return slotRepository.findByDateAndTimeAndTheme(date, time.getId(), theme.getId()) + .orElseGet(() -> slotRepository.save(Slot.create(new ReservationDate(date), time, theme))); + } + private Reservation reservation(String name, LocalDate date, ReservationTime time, Theme theme) { return reservation(name, date, time, theme, Status.APPROVED); } private Reservation reservation(String name, LocalDate date, ReservationTime time, Theme theme, Status status) { - return Reservation.reserve(new ReservationName(name), new ReservationDate(date), time, theme, - status, LocalDateTime.now(FIXED_CLOCK)); + Slot slot = giveSlot(date, time, theme); + return Reservation.reserve(new ReservationName(name), slot, status, LocalDateTime.now(FIXED_CLOCK)); } @Nested diff --git a/src/test/java/roomescape/repository/SlotRepositoryTest.java b/src/test/java/roomescape/repository/SlotRepositoryTest.java new file mode 100644 index 0000000000..9e7526aaa7 --- /dev/null +++ b/src/test/java/roomescape/repository/SlotRepositoryTest.java @@ -0,0 +1,135 @@ +package roomescape.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import java.time.LocalDate; +import java.time.LocalTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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 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.ThumbnailUrl; + +@JdbcTest +@Import(value = {SlotRepository.class, ReservationTimeRepository.class, ThemeRepository.class}) +class SlotRepositoryTest { + private static final LocalDate FUTURE = LocalDate.of(2099, 1, 1); + + @Autowired + private SlotRepository slotRepository; + + @Autowired + private ReservationTimeRepository timeRepository; + + @Autowired + private ThemeRepository themeRepository; + + private ReservationTime giveTime(int hour) { + return timeRepository.save(ReservationTime.of(LocalTime.of(hour, 0))); + } + + private Theme giveTheme(String name) { + return themeRepository.save( + Theme.create(new ThemeName(name), name + " 설명", new ThumbnailUrl("https://test-theme.com"))); + } + + @Nested + @DisplayName("save") + class Save { + + @Test + void 슬롯을_저장하면_ID가_부여된_슬롯이_반환된다() { + ReservationTime time = giveTime(10); + Theme theme = giveTheme("테마1"); + + Slot saved = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); + + assertSoftly(soft -> { + soft.assertThat(saved.getId()).isPositive(); + soft.assertThat(saved.getDate().getValue()).isEqualTo(FUTURE); + soft.assertThat(saved.getTime().getId()).isEqualTo(time.getId()); + soft.assertThat(saved.getTheme().getId()).isEqualTo(theme.getId()); + }); + } + } + + @Nested + @DisplayName("findById") + class FindById { + + @Test + void ID로_조회하면_슬롯이_반환된다() { + ReservationTime time = giveTime(10); + Theme theme = giveTheme("테마1"); + + Slot saved = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); + + assertThat(slotRepository.findById(saved.getId())).isPresent(); + } + + @Test + void 존재하지_않는_ID는_빈_Optional을_반환한다() { + assertThat(slotRepository.findById(Long.MAX_VALUE)).isEmpty(); + } + } + + @Nested + @DisplayName("findByDateAndTimeAndTheme") + class FindByDateAndTimeAndTheme { + + @Test + void 날짜_시간_테마가_일치하는_슬롯을_반환한다() { + ReservationTime time = giveTime(10); + Theme theme = giveTheme("테마1"); + + Slot saved = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); + + assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE, time.getId(), theme.getId())) + .isPresent() + .get() + .extracting(Slot::getId) + .isEqualTo(saved.getId()); + } + + @Test + void 조건이_하나라도_다르면_빈_Optional을_반환한다() { + ReservationTime time = giveTime(10); + Theme theme = giveTheme("테마1"); + slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); + + assertSoftly(soft -> { + soft.assertThat(slotRepository.findByDateAndTimeAndTheme( + FUTURE.plusDays(1), time.getId(), theme.getId())).isEmpty(); + soft.assertThat(slotRepository.findByDateAndTimeAndTheme( + FUTURE, time.getId() + 1, theme.getId())).isEmpty(); + soft.assertThat(slotRepository.findByDateAndTimeAndTheme( + FUTURE, time.getId(), theme.getId() + 1)).isEmpty(); + }); + } + } + + @Nested + @DisplayName("UNIQUE 제약") + class UniqueConstraint { + + @Test + void 동일한_날짜_시간_테마로_두번_저장하면_두번째는_기존_슬롯이_조회된다() { + ReservationTime time = giveTime(10); + Theme theme = giveTheme("테마1"); + + Slot first = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); + Slot found = slotRepository.findByDateAndTimeAndTheme(FUTURE, time.getId(), theme.getId()) + .orElseThrow(); + + assertThat(first.getId()).isEqualTo(found.getId()); + } + } +} diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index 619d819e74..ea4efee361 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -13,7 +13,6 @@ import java.time.LocalTime; import java.util.List; import java.util.Optional; - import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,12 +27,14 @@ import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.ReservationResult; import roomescape.domain.reservation.ReservationTime; +import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; import roomescape.domain.theme.Theme; import roomescape.domain.theme.ThemeName; import roomescape.domain.theme.ThumbnailUrl; import roomescape.repository.ReservationRepository; import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.SlotRepository; import roomescape.repository.ThemeRepository; @ExtendWith(MockitoExtension.class) @@ -41,12 +42,14 @@ class ReservationServiceTest { private static final String URL = "https://zeze.com/thumb.jpg"; private static final String NAME = "제제"; private static final LocalDateTime TODAY = LocalDateTime.of(2026, 5, 10, 10, 0, 0); + private static final ReservationTime DUMMY_TIME = ReservationTime.of(1L, LocalTime.of(10, 0)); + private static final Theme DUMMY_THEME = Theme.load(1L, new ThemeName("any"), "any", new ThumbnailUrl(URL)); + private static final Slot DUMMY_SLOT = Slot.load(1L, new ReservationDate(LocalDate.of(2099, 1, 1)), + DUMMY_TIME, DUMMY_THEME); private static final Reservation DUMMY = Reservation.load( 1L, new ReservationName(NAME), - new ReservationDate(LocalDate.of(2099, 1, 1)), - ReservationTime.of(1L, LocalTime.of(10, 0)), - Theme.load(1L, new ThemeName("any"), "any", new ThumbnailUrl(URL)), + DUMMY_SLOT, Status.APPROVED, TODAY ); @@ -62,6 +65,9 @@ class ReservationServiceTest { @Mock private ThemeRepository themeRepository; + @Mock + private SlotRepository slotRepository; + @InjectMocks private ReservationService reservationService; @@ -79,15 +85,8 @@ class ReservationServiceTest { @Test void APPROVED_예약_취소_시_첫번째_WAITING_예약이_승격된다() { - Reservation waiting = Reservation.load( - 2L, - new ReservationName("대기자"), - new ReservationDate(LocalDate.of(2099, 1, 1)), - ReservationTime.of(1L, LocalTime.of(10, 0)), - Theme.load(1L, new ThemeName("any"), "any", new ThumbnailUrl(URL)), - Status.WAITING, - TODAY - ); + Reservation waiting = Reservation.load(2L, new ReservationName("대기자"), DUMMY_SLOT, + Status.WAITING, TODAY); given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); given(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(anyLong(), anyLong(), any())) .willReturn(Optional.of(waiting)); @@ -99,15 +98,8 @@ class ReservationServiceTest { @Test void WAITING_예약_취소_시_승격이_발생하지_않는다() { - Reservation waiting = Reservation.load( - 2L, - new ReservationName("대기자"), - new ReservationDate(LocalDate.of(2099, 1, 1)), - ReservationTime.of(1L, LocalTime.of(10, 0)), - Theme.load(1L, new ThemeName("any"), "any", new ThumbnailUrl(URL)), - Status.WAITING, - TODAY - ); + Reservation waiting = Reservation.load(2L, new ReservationName("대기자"), DUMMY_SLOT, + Status.WAITING, TODAY); given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); reservationService.cancel(2L, LocalDateTime.MIN); @@ -144,6 +136,8 @@ class ReservationServiceTest { given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(false); + given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( + Optional.of(DUMMY_SLOT)); Assertions.assertThatThrownBy(() -> reservationService.reserve(request, LocalDateTime.MAX)); } @@ -157,6 +151,8 @@ class ReservationServiceTest { given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(false); + given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( + Optional.of(DUMMY_SLOT)); given(reservationRepository.save(any())).willReturn(DUMMY); given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); @@ -173,6 +169,8 @@ class ReservationServiceTest { given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(false); + given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( + Optional.of(DUMMY_SLOT)); Assertions.assertThatThrownBy( () -> reservationService.reserve(request, LocalDateTime.of(2026, 4, 5, 11, 0, 1))); @@ -187,6 +185,8 @@ class ReservationServiceTest { given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(false); + given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( + Optional.of(DUMMY_SLOT)); given(reservationRepository.save(any())).willReturn(DUMMY); given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); @@ -198,14 +198,16 @@ class ReservationServiceTest { void APPROVED가_이미_있으면_WAITING으로_예약된다() { ReservationTime reservationTime = ReservationTime.of(LocalTime.parse("11:00")); Theme theme = Theme.load(1L, new ThemeName("테마1"), "설명", new ThumbnailUrl(URL)); - Reservation waitingSaved = Reservation.load(2L, - new ReservationName("zeze"), new ReservationDate(LocalDate.parse("2026-04-05")), - reservationTime, theme, Status.WAITING, TODAY); + Slot waitingSlot = Slot.load(2L, new ReservationDate(LocalDate.parse("2026-04-05")), reservationTime, theme); + Reservation waitingSaved = Reservation.load(2L, new ReservationName("zeze"), waitingSlot, + Status.WAITING, TODAY); ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.parse("2026-04-05"), 1L, 1L); given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(true); + given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( + Optional.of(waitingSlot)); given(reservationRepository.save(any())).willReturn(waitingSaved); given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn( List.of(DUMMY, waitingSaved)); @@ -337,15 +339,8 @@ class ReservationServiceTest { @Test void 두번째_이후_예약은_대기_상태이다() { - Reservation waiting = Reservation.load( - 2L, - new ReservationName("대기자"), - new ReservationDate(LocalDate.of(2099, 1, 1)), - ReservationTime.of(1L, LocalTime.of(10, 0)), - Theme.load(1L, new ThemeName("any"), "any", new ThumbnailUrl(URL)), - Status.WAITING, - TODAY - ); + Reservation waiting = Reservation.load(2L, new ReservationName("대기자"), DUMMY_SLOT, + Status.WAITING, TODAY); given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())) .willReturn(List.of(DUMMY, waiting)); From 32a096e2ac8fb305e76e0ccc5bc504ef62973037 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Wed, 3 Jun 2026 16:09:12 +0900 Subject: [PATCH 03/26] =?UTF-8?q?refactor:=20=EC=B2=AB=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=EC=9D=B4=20=EB=8F=99=EC=8B=9C=EC=97=90=20=EB=93=A4?= =?UTF-8?q?=EC=96=B4=EC=98=AC=20=EB=95=8C=20=ED=95=9C=20=EA=B1=B4=EB=A7=8C?= =?UTF-8?q?=20=EC=8A=B9=EC=9D=B8=EB=90=A8=EC=9D=84=20=EB=B3=B4=EC=9E=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReservationService.java | 41 ++++++--- .../roomescape/RoomescapeApplicationTest.java | 4 +- .../domain/reservation/RankTest.java | 2 +- .../ReservationServiceIntegrationTest.java | 88 +++++++++++++++++++ .../service/ReservationServiceTest.java | 4 +- 5 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 src/test/java/roomescape/service/ReservationServiceIntegrationTest.java diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 6440c23dbc..eb44d7b510 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.controller.dto.request.ReservationCreateRequest; @@ -46,13 +47,23 @@ public ReservationResult reserve(ReservationCreateRequest request, LocalDateTime ReservationTime reservationTime = findReservationTimeByTimeId(request.getTimeId()); Theme theme = findThemeByThemeId(request.getThemeId()); + validateIsDuplicateNameReservation(request.getTimeId(), request.getThemeId(), request.getDate(), + request.getName()); Status status = determineStatus(request.getTimeId(), request.getThemeId(), request.getDate()); - Slot slot = findOrCreateSlot(new ReservationDate(request.getDate()), reservationTime, theme); - Reservation reservation = Reservation.reserve(new ReservationName(request.getName()), slot, status, now); + Slot slot = null; + try { + slot = slotRepository.findByDateAndTimeAndTheme(request.getDate(), reservationTime.getId(), + theme.getId()).orElseGet(() -> slotRepository.save( + Slot.create(new ReservationDate(request.getDate()), reservationTime, theme))); - validateIsDuplicateNameReservation(request.getTimeId(), request.getThemeId(), request.getDate(), - request.getName()); + } catch (DataIntegrityViolationException e) { + slot = slotRepository.findByDateAndTimeAndTheme(request.getDate(), reservationTime.getId(), theme.getId()) + .get(); + status = Status.WAITING; + } + + Reservation reservation = Reservation.reserve(new ReservationName(request.getName()), slot, status, now); Reservation saved = reservationRepository.save(reservation); Reservations reservations = new Reservations(reservationRepository.findByTimeAndThemeAndDate( @@ -100,19 +111,29 @@ private List findListByName(String name) { @Transactional public ReservationResult update(ReservationUpdateRequest request, long id, LocalDateTime now) { - Reservation reservation = findReservationById(id); - reservation.ensureNotPast(now); + Reservation originReservation = findReservationById(id); + originReservation.ensureNotPast(now); - ReservationDate reservationDate = new ReservationDate(request.getDate()); - ReservationTime reservationTime = findReservationTimeByTimeId(request.getTimeId()); + ReservationDate reservationDateToUpdate = new ReservationDate(request.getDate()); + ReservationTime reservationTimeToUpdate = findReservationTimeByTimeId(request.getTimeId()); validateIsDuplicateNameReservation(request.getTimeId(), request.getThemeId(), request.getDate(), request.getName()); - Slot slot = findOrCreateSlot(reservationDate, reservationTime, reservation.getTheme()); - Reservation target = Reservation.reserve(reservation.getName(), slot, reservation.getStatus(), now); + Slot slotToUpdate = findOrCreateSlot(reservationDateToUpdate, reservationTimeToUpdate, + originReservation.getTheme()); + + Status status = determineStatus(request.getTimeId(), request.getThemeId(), request.getDate()); + Reservation target = Reservation.reserve(originReservation.getName(), slotToUpdate, status, now); + Reservation updated = reservationRepository.update(id, target); + reservationRepository.findFirstWaitingByTimeAndThemeAndDate( + originReservation.getTime().getId(), + originReservation.getTheme().getId(), + originReservation.getDate().getValue() + ).ifPresent(waiting -> reservationRepository.updateStatus(waiting.getId(), Status.APPROVED)); + Reservations reservations = new Reservations(reservationRepository.findByTimeAndThemeAndDate( updated.getTime(), updated.getTheme(), updated.getDate())); diff --git a/src/test/java/roomescape/RoomescapeApplicationTest.java b/src/test/java/roomescape/RoomescapeApplicationTest.java index 8023fd34d3..1e6f8643d1 100644 --- a/src/test/java/roomescape/RoomescapeApplicationTest.java +++ b/src/test/java/roomescape/RoomescapeApplicationTest.java @@ -194,7 +194,7 @@ private int availableCount(String date, long themeId) { .when().get("/reservations/" + id) .then().statusCode(200) .body("state", org.hamcrest.Matchers.equalTo("승인")) - .body("rank", org.hamcrest.Matchers.equalTo(1)); + .body("rank", org.hamcrest.Matchers.equalTo(0)); } @Test @@ -207,7 +207,7 @@ private int availableCount(String date, long themeId) { .when().get("/reservations/" + waitingId) .then().statusCode(200) .body("state", org.hamcrest.Matchers.equalTo("대기")) - .body("rank", org.hamcrest.Matchers.equalTo(2)); + .body("rank", org.hamcrest.Matchers.equalTo(1)); } @Test diff --git a/src/test/java/roomescape/domain/reservation/RankTest.java b/src/test/java/roomescape/domain/reservation/RankTest.java index 93821cb448..dc68b39aa3 100644 --- a/src/test/java/roomescape/domain/reservation/RankTest.java +++ b/src/test/java/roomescape/domain/reservation/RankTest.java @@ -13,7 +13,7 @@ class RankTest { } @ParameterizedTest - @ValueSource(ints = {0, -1}) + @ValueSource(ints = {-999, -1}) void 잘못된_입력은_예외가_발생한다(int value) { Assertions.assertThatException().isThrownBy(() -> new Rank(value)).isInstanceOf(RoomEscapeException.class); } diff --git a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java new file mode 100644 index 0000000000..13e1e75953 --- /dev/null +++ b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java @@ -0,0 +1,88 @@ +package roomescape.service; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import io.restassured.RestAssured; +import java.time.LocalDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; +import roomescape.RoomEscapeFixture; +import roomescape.controller.dto.request.ReservationCreateRequest; +import roomescape.domain.reservation.ReservationName; +import roomescape.domain.reservation.ReservationResult; +import roomescape.domain.reservation.Status; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +class ReservationServiceIntegrationTest { + @Autowired + private ReservationService reservationService; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @LocalServerPort + int port; + + @BeforeEach + void init() { + RestAssured.port = port; + jdbcTemplate.update("insert into reservation_time(start_at) values ('10:00')"); + jdbcTemplate.update( + "insert into theme(name, description, thumbnail_url) values ('공포', '무서워요', 'https://zeze.com')"); + } + + @Test + void 동시에_10명이_첫_예약_요청시_1명만_승인상태가_된다() throws Exception { + // 한 슬롯에 Approve된 예약은 반드시 1건 미만이어야 한다. + int threads = 10; + var ready = new CountDownLatch(threads); + var start = new CountDownLatch(1); + var done = new CountDownLatch(threads); + var approved = new AtomicInteger(); + var waiting = new AtomicInteger(); + + var pool = Executors.newFixedThreadPool(threads); + + for (int i = 0; i < threads; i++) { + ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequestWithName( + new ReservationName(i + "")); + pool.submit(() -> { + ready.countDown(); + try { + start.await(); + ReservationResult result = reservationService.reserve(request, LocalDateTime.now()); + + if (result.getReservation().getStatus() == Status.APPROVED) { + approved.incrementAndGet(); + } + if (result.getReservation().getStatus() == Status.WAITING) { + waiting.incrementAndGet(); + } + } catch (DuplicateKeyException ignored) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + done.countDown(); + } + }); + } + ready.await(); + start.countDown(); + done.await(); + + assertThat(approved.get()).isEqualTo(1); + assertThat(waiting.get()).isEqualTo(9); + } +} + diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index ea4efee361..42163e3056 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -293,7 +293,7 @@ class ReservationServiceTest { ReservationResult result = reservationService.find(EXISTS_ID); Assertions.assertThat(result.getReservation().getId()).isEqualTo(EXISTS_ID); - Assertions.assertThat(result.getRank().getValue()).isEqualTo(1); + Assertions.assertThat(result.getRank().getValue()).isEqualTo(0); } @Test @@ -348,6 +348,6 @@ class ReservationServiceTest { ReservationResult result = reservationService.find(2L); Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.WAITING); - Assertions.assertThat(result.getRank().getValue()).isEqualTo(2); + Assertions.assertThat(result.getRank().getValue()).isEqualTo(1); } } From 7355af4eb5306b5bef0b757b47fc2ace975d3d4d Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Wed, 3 Jun 2026 18:23:06 +0900 Subject: [PATCH 04/26] =?UTF-8?q?refactor:=20=EC=88=9C=EC=9C=84=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EC=9D=84=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=88=98=ED=96=89=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReservationController.java | 10 ++-- .../dto/response/ReservationResponse.java | 10 ++-- .../roomescape/domain/reservation/Rank.java | 15 ++++++ .../domain/reservation/RankedReservation.java | 28 +++++++++++ .../reservation/RankedReservations.java | 39 +++++++++++++++ .../domain/reservation/Reservation.java | 12 +++++ .../domain/reservation/ReservationResult.java | 19 ------- .../roomescape/domain/reservation/Slot.java | 4 ++ .../service/ReservationService.java | 46 ++++++----------- .../java/roomescape/RoomEscapeFixture.java | 24 +++++++-- .../reservation/RankedReservationTest.java | 25 ++++++++++ .../reservation/RankedReservationsTest.java | 49 +++++++++++++++++++ .../domain/reservation/ReservationTest.java | 18 +++++++ .../ReservationServiceIntegrationTest.java | 4 +- .../service/ReservationServiceTest.java | 18 +++---- 15 files changed, 244 insertions(+), 77 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservation/RankedReservation.java create mode 100644 src/main/java/roomescape/domain/reservation/RankedReservations.java delete mode 100644 src/main/java/roomescape/domain/reservation/ReservationResult.java create mode 100644 src/test/java/roomescape/domain/reservation/RankedReservationTest.java create mode 100644 src/test/java/roomescape/domain/reservation/RankedReservationsTest.java diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java index 762d3abeef..24b9171856 100644 --- a/src/main/java/roomescape/controller/ReservationController.java +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -16,7 +16,7 @@ import roomescape.controller.dto.request.ReservationCreateRequest; import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.controller.dto.response.ReservationResponse; -import roomescape.domain.reservation.ReservationResult; +import roomescape.domain.reservation.RankedReservation; import roomescape.service.ReservationService; @RestController @@ -30,7 +30,7 @@ public ReservationController(ReservationService reservationService) { @PostMapping("/reservations") @ResponseStatus(HttpStatus.CREATED) public ReservationResponse create(@Valid @RequestBody ReservationCreateRequest request) { - ReservationResult reservation = reservationService.reserve(request, LocalDateTime.now()); + RankedReservation reservation = reservationService.reserve(request, LocalDateTime.now()); return ReservationResponse.from(reservation); } @@ -38,7 +38,7 @@ public ReservationResponse create(@Valid @RequestBody ReservationCreateRequest r @GetMapping("/reservations") @ResponseStatus(HttpStatus.OK) public List findList(@RequestParam(required = false) String name) { - List reservations = reservationService.findList(name); + List reservations = reservationService.findList(name); return reservations.stream() .map(ReservationResponse::from) @@ -48,7 +48,7 @@ public List findList(@RequestParam(required = false) String @GetMapping("/reservations/{id}") @ResponseStatus(HttpStatus.OK) public ReservationResponse find(@PathVariable long id) { - ReservationResult reservation = reservationService.find(id); + RankedReservation reservation = reservationService.find(id); return ReservationResponse.from(reservation); } @@ -61,7 +61,7 @@ public void delete(@PathVariable Long id) { @PutMapping("/reservations/{id}") @ResponseStatus(HttpStatus.OK) public ReservationResponse update(@Valid @RequestBody ReservationUpdateRequest request, @PathVariable long id) { - ReservationResult updated = reservationService.update(request, id, LocalDateTime.now()); + RankedReservation updated = reservationService.update(request, id, LocalDateTime.now()); return ReservationResponse.from(updated); } } diff --git a/src/main/java/roomescape/controller/dto/response/ReservationResponse.java b/src/main/java/roomescape/controller/dto/response/ReservationResponse.java index dc9a314632..241717d186 100644 --- a/src/main/java/roomescape/controller/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/controller/dto/response/ReservationResponse.java @@ -1,8 +1,8 @@ package roomescape.controller.dto.response; import java.time.LocalDate; +import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservation.ReservationResult; public class ReservationResponse { private final long id; @@ -25,12 +25,12 @@ public ReservationResponse(long id, String name, LocalDate date, String state, i this.theme = theme; } - public static ReservationResponse from(ReservationResult reservationResult) { - Reservation reservation = reservationResult.getReservation(); + public static ReservationResponse from(RankedReservation rankedReservation) { + Reservation reservation = rankedReservation.getReservation(); return new ReservationResponse(reservation.getId(), reservation.getName().getValue(), reservation.getDate().getValue(), - reservationResult.getReservation().getStatus().getKoreanName(), - reservationResult.getRank().getValue(), + rankedReservation.getReservation().getStatus().getKoreanName(), + rankedReservation.getRank().getValue(), ReservationTimeResponse.from(reservation.getTime()), ThemeResponse.from(reservation.getTheme())); } diff --git a/src/main/java/roomescape/domain/reservation/Rank.java b/src/main/java/roomescape/domain/reservation/Rank.java index 4caea99698..7469f0a70b 100644 --- a/src/main/java/roomescape/domain/reservation/Rank.java +++ b/src/main/java/roomescape/domain/reservation/Rank.java @@ -2,6 +2,7 @@ import common.exception.ErrorCode; import common.exception.RoomEscapeException; +import java.util.Objects; public class Rank { private static final int MIN_RANK_VALUE = 0; @@ -22,4 +23,18 @@ private void validate(int value) { public int getValue() { return value; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Rank rank = (Rank) o; + return value == rank.value; + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } } diff --git a/src/main/java/roomescape/domain/reservation/RankedReservation.java b/src/main/java/roomescape/domain/reservation/RankedReservation.java new file mode 100644 index 0000000000..2d23dbd024 --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/RankedReservation.java @@ -0,0 +1,28 @@ +package roomescape.domain.reservation; + +import java.util.List; + +public class RankedReservation { + private final Rank rank; + private final Reservation reservation; + + public RankedReservation(Rank rank, Reservation reservation) { + this.rank = rank; + this.reservation = reservation; + } + + public static RankedReservation decideRankFrom(Reservation target, List reservations) { + long earlierCount = reservations.stream() + .filter(r -> r.isEarlierThan(target)) + .count(); + return new RankedReservation(new Rank((int) earlierCount), target); + } + + public Rank getRank() { + return rank; + } + + public Reservation getReservation() { + return reservation; + } +} diff --git a/src/main/java/roomescape/domain/reservation/RankedReservations.java b/src/main/java/roomescape/domain/reservation/RankedReservations.java new file mode 100644 index 0000000000..1bbe86cacc --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/RankedReservations.java @@ -0,0 +1,39 @@ +package roomescape.domain.reservation; + +import java.util.List; + +public class RankedReservations { + private final List reservations; + + public RankedReservations(List reservations) { + this.reservations = reservations; + } + + public List resultsOf(String name) { + List listByName = getListByName(name); + + return listByName.stream() + .map(this::toRankedReservation) + .toList(); + } + + private RankedReservation toRankedReservation(Reservation target) { + List sameSlots = reservations.stream() + .filter(reservation -> reservation.isSameSlot(target)) + .toList(); + + return RankedReservation.decideRankFrom(target, sameSlots); + } + + private List getListByName(String name) { + return reservations.stream() + .filter(reservation -> reservation.getName().equals(new ReservationName(name))) + .toList(); + } + + public List resultsOf() { + return reservations.stream() + .map(this::toRankedReservation) + .toList(); + } +} diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index c2461d16e5..26ce6a9092 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -41,6 +41,18 @@ public void ensureNotPast(LocalDateTime now) { } } + public boolean isEarlierThan(Reservation target) { + int byTime = createdAt.compareTo(target.getCreatedAt()); + if (byTime != 0) { + return byTime < 0; + } + return id < target.getId(); + } + + public boolean isSameSlot(Reservation target) { + return slot.isSame(target); + } + public long getId() { return id; } diff --git a/src/main/java/roomescape/domain/reservation/ReservationResult.java b/src/main/java/roomescape/domain/reservation/ReservationResult.java deleted file mode 100644 index 538a6d0023..0000000000 --- a/src/main/java/roomescape/domain/reservation/ReservationResult.java +++ /dev/null @@ -1,19 +0,0 @@ -package roomescape.domain.reservation; - -public class ReservationResult { - private final Rank rank; - private final Reservation reservation; - - public ReservationResult(Rank rank, Reservation reservation) { - this.rank = rank; - this.reservation = reservation; - } - - public Rank getRank() { - return rank; - } - - public Reservation getReservation() { - return reservation; - } -} diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index 3b52eb683a..436fb29347 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -28,6 +28,10 @@ public Slot withId(long id) { return new Slot(id, date, time, theme); } + public boolean isSame(Reservation target) { + return id == target.getSlot().getId(); + } + public long getId() { return id; } diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index eb44d7b510..07881bcd55 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -4,7 +4,6 @@ import common.exception.RoomEscapeException; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; @@ -12,10 +11,11 @@ import roomescape.controller.dto.request.ReservationCreateRequest; import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.domain.reservation.Rank; +import roomescape.domain.reservation.RankedReservation; +import roomescape.domain.reservation.RankedReservations; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationName; -import roomescape.domain.reservation.ReservationResult; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; @@ -43,7 +43,7 @@ public ReservationService(ReservationRepository reservationRepository, } @Transactional - public ReservationResult reserve(ReservationCreateRequest request, LocalDateTime now) { + public RankedReservation reserve(ReservationCreateRequest request, LocalDateTime now) { ReservationTime reservationTime = findReservationTimeByTimeId(request.getTimeId()); Theme theme = findThemeByThemeId(request.getThemeId()); @@ -71,46 +71,30 @@ public ReservationResult reserve(ReservationCreateRequest request, LocalDateTime Rank rank = reservations.rankOf(saved); - return new ReservationResult(rank, saved); + return new RankedReservation(rank, saved); } - public ReservationResult find(long reservationId) { + public RankedReservation find(long reservationId) { Reservation reservation = findReservationById(reservationId); - Reservations reservations = new Reservations(reservationRepository.findByTimeAndThemeAndDate( - reservation.getTime(), reservation.getTheme(), reservation.getDate())); - - Rank rank = reservations.rankOf(reservation); + List sameSlots = reservationRepository.findByTimeAndThemeAndDate( + reservation.getTime(), reservation.getTheme(), reservation.getDate()); - return new ReservationResult(rank, reservation); + return RankedReservation.decideRankFrom(reservation, sameSlots); } - public List findList(String name) { - List reservations = findListByName(name); - List reservationResults = new ArrayList<>(); - - for (Reservation reservation : reservations) { - Reservations sameScheduleReservations = new Reservations(reservationRepository.findByTimeAndThemeAndDate( - reservation.getTime(), reservation.getTheme(), reservation.getDate())); + public List findList(String name) { + RankedReservations rankedReservations = new RankedReservations(reservationRepository.findAll()); - Rank rank = sameScheduleReservations.rankOf(reservation); - - ReservationResult result = new ReservationResult(rank, reservation); - reservationResults.add(result); - } - - return reservationResults; - } - - private List findListByName(String name) { if (name == null) { - return reservationRepository.findAll(); + return rankedReservations.resultsOf(); } - return reservationRepository.findAllByName(name); + return rankedReservations.resultsOf(name); } + @Transactional - public ReservationResult update(ReservationUpdateRequest request, long id, LocalDateTime now) { + public RankedReservation update(ReservationUpdateRequest request, long id, LocalDateTime now) { Reservation originReservation = findReservationById(id); originReservation.ensureNotPast(now); @@ -139,7 +123,7 @@ public ReservationResult update(ReservationUpdateRequest request, long id, Local Rank rank = reservations.rankOf(updated); - return new ReservationResult(rank, updated); + return new RankedReservation(rank, updated); } @Transactional diff --git a/src/test/java/roomescape/RoomEscapeFixture.java b/src/test/java/roomescape/RoomEscapeFixture.java index e30963a293..a75032d4c7 100644 --- a/src/test/java/roomescape/RoomEscapeFixture.java +++ b/src/test/java/roomescape/RoomEscapeFixture.java @@ -10,10 +10,10 @@ import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.controller.dto.request.ThemeFamousFindRequest; import roomescape.domain.reservation.Rank; +import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationName; -import roomescape.domain.reservation.ReservationResult; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; @@ -34,6 +34,8 @@ public class RoomEscapeFixture { private static final Slot SLOT = Slot.create(FUTURE_DATE, TIME, THEME); private static final Rank APPROVE_RANK = new Rank(1); private static final Rank WAITING_RANK = new Rank(2); + private static final LocalDateTime PAST_DATE_TIME = LocalDateTime.of(2000, 11, 11, 10, 0); + private static final LocalDateTime FUTURE_DATE_TIME = LocalDateTime.of(2099, 11, 11, 10, 0); public static Theme theme() { return THEME; @@ -63,12 +65,24 @@ public static Reservation reservationWithWaiting() { return Reservation.load(2L, NAME, SLOT, Status.WAITING, LocalDateTime.now(FIXED_CLOCK)); } - public static ReservationResult reservationResultWithApproved() { - return new ReservationResult(APPROVE_RANK, reservationWithApproved()); + public static Reservation reservationWithPast() { + return Reservation.load(1L, NAME, SLOT, Status.APPROVED, PAST_DATE_TIME); } - public static ReservationResult reservationResultWithWaiting() { - return new ReservationResult(WAITING_RANK, reservationWithWaiting()); + public static Reservation reservationWithFuture() { + return Reservation.load(2L, NAME, SLOT, Status.APPROVED, FUTURE_DATE_TIME); + } + + public static Reservation reservationWithLocalDateTime(LocalDateTime dateTime) { + return Reservation.load(1L, NAME, SLOT, Status.APPROVED, dateTime); + } + + public static RankedReservation reservationResultWithApproved() { + return new RankedReservation(APPROVE_RANK, reservationWithApproved()); + } + + public static RankedReservation reservationResultWithWaiting() { + return new RankedReservation(WAITING_RANK, reservationWithWaiting()); } public static ThemeFamousFindRequest themeFamousFindRequest() { diff --git a/src/test/java/roomescape/domain/reservation/RankedReservationTest.java b/src/test/java/roomescape/domain/reservation/RankedReservationTest.java new file mode 100644 index 0000000000..c23878cb7d --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/RankedReservationTest.java @@ -0,0 +1,25 @@ +package roomescape.domain.reservation; + +import java.time.LocalDateTime; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import roomescape.RoomEscapeFixture; + +class RankedReservationTest { + + private static final Reservation TODAY = RoomEscapeFixture.reservationWithLocalDateTime( + LocalDateTime.of(2026, 6, 2, 10, 0)); + + @Test + void 예약과_예약목록으로_순번을_정하여_생성된다() { + List reservations = List.of( + RoomEscapeFixture.reservationWithLocalDateTime(LocalDateTime.of(2026, 6, 1, 10, 0)), + RoomEscapeFixture.reservationWithLocalDateTime(LocalDateTime.of(2026, 6, 1, 10, 1)), + TODAY, + RoomEscapeFixture.reservationWithLocalDateTime(LocalDateTime.of(2026, 6, 3, 10, 0)), + RoomEscapeFixture.reservationWithLocalDateTime(LocalDateTime.of(2027, 7, 2, 10, 0))); + + Assertions.assertThat(RankedReservation.decideRankFrom(TODAY, reservations).getRank().getValue()).isEqualTo(2); + } +} \ No newline at end of file diff --git a/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java b/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java new file mode 100644 index 0000000000..305450c86a --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java @@ -0,0 +1,49 @@ +package roomescape.domain.reservation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import roomescape.domain.theme.Theme; +import roomescape.domain.theme.ThemeName; +import roomescape.domain.theme.ThumbnailUrl; + +class RankedReservationsTest { + private static final Slot SLOT = Slot.load(1L, + new ReservationDate(LocalDate.of(2099, 1, 1)), + ReservationTime.of(1L, LocalTime.of(10, 0)), + Theme.load(1L, new ThemeName("테마"), "설명", new ThumbnailUrl("https://zeze.com"))); + + @Test + void 같은_슬롯에서_먼저_예약한_사람이_rank_0이다() { + Reservation first = Reservation.load(1L, new ReservationName("제제"), SLOT, Status.APPROVED, + LocalDateTime.of(2099, 1, 1, 9, 0)); + Reservation second = Reservation.load(2L, new ReservationName("달수"), SLOT, Status.WAITING, + LocalDateTime.of(2099, 1, 1, 9, 1)); + + RankedReservations rankedReservations = new RankedReservations(List.of(first, second)); + + List results = rankedReservations.resultsOf(); + + assertThat(results.get(0).getRank().getValue()).isEqualTo(0); + assertThat(results.get(1).getRank().getValue()).isEqualTo(1); + } + + @Test + void 이름으로_조회하면_해당_이름의_예약만_반환된다() { + Reservation r1 = Reservation.load(1L, new ReservationName("제제"), SLOT, Status.APPROVED, + LocalDateTime.of(2099, 1, 1, 9, 0)); + Reservation r2 = Reservation.load(2L, new ReservationName("달수"), SLOT, Status.WAITING, + LocalDateTime.of(2099, 1, 1, 9, 1)); + + RankedReservations rankedReservations = new RankedReservations(List.of(r1, r2)); + + List results = rankedReservations.resultsOf("달수"); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getReservation()).isEqualTo(r2); + } +} diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 1a66273df1..287b85bb5d 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -4,6 +4,8 @@ import java.time.LocalDateTime; import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -27,4 +29,20 @@ static Stream nullCases() { Arguments.of(name, slot, null) ); } + + @Test + void 과거_예약인지_비교할_수_있다() { + Reservation past = RoomEscapeFixture.reservationWithPast(); + Reservation future = RoomEscapeFixture.reservationWithFuture(); + + Assertions.assertThat(past.isEarlierThan(future)).isTrue(); + } + + @Test + void 시점이_같을때_id가_더_작으면_false를_반환한다() { + Reservation id1WithSameDate = RoomEscapeFixture.reservationWithApproved(); + Reservation id2WithSameDate = RoomEscapeFixture.reservationWithPast(); + + Assertions.assertThat(id1WithSameDate.isEarlierThan(id2WithSameDate)).isFalse(); + } } diff --git a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java index 13e1e75953..7e0780d6c5 100644 --- a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java +++ b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java @@ -18,8 +18,8 @@ import org.springframework.test.annotation.DirtiesContext; import roomescape.RoomEscapeFixture; import roomescape.controller.dto.request.ReservationCreateRequest; +import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.ReservationName; -import roomescape.domain.reservation.ReservationResult; import roomescape.domain.reservation.Status; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -61,7 +61,7 @@ void init() { ready.countDown(); try { start.await(); - ReservationResult result = reservationService.reserve(request, LocalDateTime.now()); + RankedReservation result = reservationService.reserve(request, LocalDateTime.now()); if (result.getReservation().getStatus() == Status.APPROVED) { approved.incrementAndGet(); diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index 42163e3056..90638bce57 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -22,10 +22,10 @@ import roomescape.RoomEscapeFixture; import roomescape.controller.dto.request.ReservationCreateRequest; import roomescape.controller.dto.request.ReservationUpdateRequest; +import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationName; -import roomescape.domain.reservation.ReservationResult; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; @@ -212,7 +212,7 @@ class ReservationServiceTest { given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn( List.of(DUMMY, waitingSaved)); - ReservationResult result = reservationService.reserve(request, LocalDateTime.of(2026, 4, 5, 10, 59, 59)); + RankedReservation result = reservationService.reserve(request, LocalDateTime.of(2026, 4, 5, 10, 59, 59)); Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.WAITING); } @@ -290,7 +290,7 @@ class ReservationServiceTest { given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); - ReservationResult result = reservationService.find(EXISTS_ID); + RankedReservation result = reservationService.find(EXISTS_ID); Assertions.assertThat(result.getReservation().getId()).isEqualTo(EXISTS_ID); Assertions.assertThat(result.getRank().getValue()).isEqualTo(0); @@ -308,9 +308,8 @@ class ReservationServiceTest { @Test void 이름_없이_목록_조회시_전체_예약을_반환한다() { given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); - List results = reservationService.findList(null); + List results = reservationService.findList(null); Assertions.assertThat(results).hasSize(1); Assertions.assertThat(results.get(0).getReservation().getId()).isEqualTo(EXISTS_ID); @@ -318,10 +317,9 @@ class ReservationServiceTest { @Test void 이름으로_목록_조회시_해당_이름의_예약만_반환한다() { - given(reservationRepository.findAllByName(NAME)).willReturn(List.of(DUMMY)); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); + given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); - List results = reservationService.findList(NAME); + List results = reservationService.findList(NAME); Assertions.assertThat(results).hasSize(1); Assertions.assertThat(results.get(0).getReservation().getName().getValue()).isEqualTo(NAME); @@ -332,7 +330,7 @@ class ReservationServiceTest { given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); - ReservationResult result = reservationService.find(EXISTS_ID); + RankedReservation result = reservationService.find(EXISTS_ID); Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.APPROVED); } @@ -345,7 +343,7 @@ class ReservationServiceTest { given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())) .willReturn(List.of(DUMMY, waiting)); - ReservationResult result = reservationService.find(2L); + RankedReservation result = reservationService.find(2L); Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.WAITING); Assertions.assertThat(result.getRank().getValue()).isEqualTo(1); From f01232ed20a0126d3435bebc366f77b163bdff56 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Wed, 3 Jun 2026 20:04:22 +0900 Subject: [PATCH 05/26] =?UTF-8?q?refactor:=20RoomEscapeFixture=EC=97=90=20?= =?UTF-8?q?=EB=B9=8C=EB=8D=94=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=ED=95=98=EC=97=AC=20=EC=9C=A0=EC=97=B0=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/RoomEscapeFixture.java | 131 ++++-- .../controller/ReservationControllerTest.java | 202 -------- .../reservation/RankedReservationTest.java | 14 +- .../reservation/RankedReservationsTest.java | 31 +- .../domain/reservation/ReservationTest.java | 14 +- .../domain/reservation/SlotTest.java | 6 +- .../service/ReservationServiceTest.java | 445 +++++++----------- 7 files changed, 290 insertions(+), 553 deletions(-) delete mode 100644 src/test/java/roomescape/controller/ReservationControllerTest.java diff --git a/src/test/java/roomescape/RoomEscapeFixture.java b/src/test/java/roomescape/RoomEscapeFixture.java index a75032d4c7..342196af76 100644 --- a/src/test/java/roomescape/RoomEscapeFixture.java +++ b/src/test/java/roomescape/RoomEscapeFixture.java @@ -2,7 +2,6 @@ import java.time.Clock; import java.time.Instant; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; @@ -22,67 +21,39 @@ import roomescape.domain.theme.ThumbnailUrl; public class RoomEscapeFixture { - private final static Clock FIXED_CLOCK = Clock.fixed( + static final Clock FIXED_CLOCK = Clock.fixed( Instant.parse("2026-05-10T03:00:00Z"), ZoneId.of("Asia/Seoul") ); - private static final ReservationName NAME = new ReservationName("zeze"); - private static final ReservationDate FUTURE_DATE = new ReservationDate(LocalDate.of(2099, 11, 11)); - private static final ReservationDate PAST_DATE = new ReservationDate(LocalDate.of(2000, 11, 11)); - private static final ReservationTime TIME = ReservationTime.of(LocalTime.of(10, 0)); - private static final Theme THEME = Theme.create(new ThemeName("공포"), "무서워요", new ThumbnailUrl("https://zeze.com")); - private static final Slot SLOT = Slot.create(FUTURE_DATE, TIME, THEME); + public static final LocalDateTime PAST_DATE_TIME = LocalDateTime.of(2000, 11, 11, 10, 0); + public static final LocalDateTime FUTURE_DATE_TIME = LocalDateTime.of(2099, 11, 11, 10, 0); + + static final ReservationName NAME = new ReservationName("zeze"); + static final ReservationDate FUTURE_DATE = new ReservationDate(FUTURE_DATE_TIME.toLocalDate()); + static final ReservationDate PAST_DATE = new ReservationDate(PAST_DATE_TIME.toLocalDate()); + static final ReservationTime TIME = ReservationTime.of(LocalTime.of(10, 0)); + static final Theme THEME = Theme.create(new ThemeName("공포"), "무서워요", new ThumbnailUrl("https://zeze.com")); private static final Rank APPROVE_RANK = new Rank(1); private static final Rank WAITING_RANK = new Rank(2); - private static final LocalDateTime PAST_DATE_TIME = LocalDateTime.of(2000, 11, 11, 10, 0); - private static final LocalDateTime FUTURE_DATE_TIME = LocalDateTime.of(2099, 11, 11, 10, 0); - public static Theme theme() { - return THEME; - } - - public static ReservationName reservationName() { - return NAME; - } - - public static ReservationDate reservationDate() { - return FUTURE_DATE; - } - - public static ReservationTime reservationTime() { - return TIME; - } - - public static Slot slot() { - return SLOT; + public static SlotBuilder slot() { + return new SlotBuilder(); } - public static Reservation reservationWithApproved() { - return Reservation.load(1L, NAME, SLOT, Status.APPROVED, LocalDateTime.now(FIXED_CLOCK)); + public static ReservationBuilder reservation() { + return new ReservationBuilder(); } - public static Reservation reservationWithWaiting() { - return Reservation.load(2L, NAME, SLOT, Status.WAITING, LocalDateTime.now(FIXED_CLOCK)); - } - - public static Reservation reservationWithPast() { - return Reservation.load(1L, NAME, SLOT, Status.APPROVED, PAST_DATE_TIME); - } - - public static Reservation reservationWithFuture() { - return Reservation.load(2L, NAME, SLOT, Status.APPROVED, FUTURE_DATE_TIME); - } - - public static Reservation reservationWithLocalDateTime(LocalDateTime dateTime) { - return Reservation.load(1L, NAME, SLOT, Status.APPROVED, dateTime); + public static Theme theme() { + return THEME; } public static RankedReservation reservationResultWithApproved() { - return new RankedReservation(APPROVE_RANK, reservationWithApproved()); + return new RankedReservation(APPROVE_RANK, reservation().build()); } public static RankedReservation reservationResultWithWaiting() { - return new RankedReservation(WAITING_RANK, reservationWithWaiting()); + return new RankedReservation(WAITING_RANK, reservation().id(2L).status(Status.WAITING).build()); } public static ThemeFamousFindRequest themeFamousFindRequest() { @@ -120,4 +91,72 @@ public static ReservationUpdateRequest reservationUpdateRequest() { public static ReservationUpdateRequest reservationUpdateRequestWithPastDate() { return new ReservationUpdateRequest(NAME.getValue(), PAST_DATE.getValue(), 1L, 1L); } + + public static class SlotBuilder { + private long id = 1L; + private ReservationDate date = FUTURE_DATE; + private ReservationTime time = TIME; + private Theme theme = THEME; + + public SlotBuilder id(long id) { + this.id = id; + return this; + } + + public SlotBuilder date(ReservationDate date) { + this.date = date; + return this; + } + + public SlotBuilder time(ReservationTime time) { + this.time = time; + return this; + } + + public SlotBuilder theme(Theme theme) { + this.theme = theme; + return this; + } + + public Slot build() { + return Slot.load(id, date, time, theme); + } + } + + public static class ReservationBuilder { + private long id = 1L; + private ReservationName name = NAME; + private Slot slot = RoomEscapeFixture.slot().build(); + private Status status = Status.APPROVED; + private LocalDateTime createdAt = LocalDateTime.now(FIXED_CLOCK); + + public ReservationBuilder id(long id) { + this.id = id; + return this; + } + + public ReservationBuilder name(String name) { + this.name = new ReservationName(name); + return this; + } + + public ReservationBuilder slot(Slot slot) { + this.slot = slot; + return this; + } + + public ReservationBuilder status(Status status) { + this.status = status; + return this; + } + + public ReservationBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public Reservation build() { + return Reservation.load(id, name, slot, status, createdAt); + } + } } diff --git a/src/test/java/roomescape/controller/ReservationControllerTest.java b/src/test/java/roomescape/controller/ReservationControllerTest.java deleted file mode 100644 index 5682d54e90..0000000000 --- a/src/test/java/roomescape/controller/ReservationControllerTest.java +++ /dev/null @@ -1,202 +0,0 @@ -package roomescape.controller; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.databind.ObjectMapper; -import common.exception.ErrorCode; -import common.exception.RoomEscapeException; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.http.MediaType; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.web.servlet.MockMvc; -import roomescape.RoomEscapeFixture; -import roomescape.controller.dto.request.ReservationCreateRequest; -import roomescape.controller.dto.request.ReservationUpdateRequest; -import roomescape.service.ReservationService; - -@WebMvcTest(ReservationController.class) -class ReservationControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @MockitoBean - private ReservationService reservationService; - - @Test - void 예약_생성_성공시_201을_반환한다() throws Exception { - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequest(); - given(reservationService.reserve(any(), any())).willReturn(RoomEscapeFixture.reservationResultWithApproved()); - - mockMvc.perform(post("/reservations") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id").value(1)) - .andExpect(jsonPath("$.name").value("zeze")) - .andExpect(jsonPath("$.state").value("승인")) - .andExpect(jsonPath("$.rank").value(1)); - } - - @Test - void 예약_생성시_이름이_없으면_400을_반환한다() throws Exception { - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequestWithNullName(); - - mockMvc.perform(post("/reservations") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - } - - @Test - void 예약_생성시_날짜가_없으면_400을_반환한다() throws Exception { - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequestWithNullDate(); - - mockMvc.perform(post("/reservations") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - } - - @Test - void 예약_생성시_TimeId가_없으면_400을_반환한다() throws Exception { - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequestWithNullTimeId(); - - mockMvc.perform(post("/reservations") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()); - } - - @Test - void 예약_생성시_서비스에서_중복_예외_발생시_409를_반환한다() throws Exception { - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequest(); - given(reservationService.reserve(any(), any())) - .willThrow(new RoomEscapeException(ErrorCode.DUPLICATE_RESERVATION)); - - mockMvc.perform(post("/reservations") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isConflict()); - } - - @Test - void 예약_생성시_과거_날짜면_422를_반환한다() throws Exception { - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequestWithPastDate(); - given(reservationService.reserve(any(), any())) - .willThrow(new RoomEscapeException(ErrorCode.PAST_RESERVATION_NOT_ALLOWED)); - - mockMvc.perform(post("/reservations") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isUnprocessableEntity()); - } - - @Test - void 예약_전체_목록_조회_성공시_200을_반환한다() throws Exception { - given(reservationService.findList(null)).willReturn(List.of( - RoomEscapeFixture.reservationResultWithApproved(), - RoomEscapeFixture.reservationResultWithWaiting())); - - mockMvc.perform(get("/reservations")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(2)); - } - - @Test - void 이름으로_예약_목록_조회_성공시_200을_반환한다() throws Exception { - given(reservationService.findList("zeze")).willReturn( - List.of(RoomEscapeFixture.reservationResultWithApproved())); - - mockMvc.perform(get("/reservations").param("name", "zeze")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(1)) - .andExpect(jsonPath("$[0].name").value("zeze")); - } - - @Test - void 예약_단건_조회_성공시_200을_반환한다() throws Exception { - given(reservationService.find(1L)).willReturn(RoomEscapeFixture.reservationResultWithApproved()); - - mockMvc.perform(get("/reservations/1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(1)); - } - - @Test - void 없는_예약_단건_조회시_404를_반환한다() throws Exception { - given(reservationService.find(999L)) - .willThrow(new RoomEscapeException(ErrorCode.RESERVATION_NOT_FOUND)); - - mockMvc.perform(get("/reservations/999")) - .andExpect(status().isNotFound()); - } - - @Test - void 예약_삭제_성공시_200을_반환한다() throws Exception { - mockMvc.perform(delete("/reservations/1")) - .andExpect(status().isOk()); - } - - @Test - void 예약_수정_성공시_200을_반환한다() throws Exception { - ReservationUpdateRequest request = RoomEscapeFixture.reservationUpdateRequest(); - given(reservationService.update(any(), anyLong(), any())).willReturn( - RoomEscapeFixture.reservationResultWithApproved()); - - mockMvc.perform(put("/reservations/1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(1)); - } - - @Test - void 예약_수정시_존재하지_않는_예약이면_404를_반환한다() throws Exception { - ReservationUpdateRequest request = RoomEscapeFixture.reservationUpdateRequest(); - ; - given(reservationService.update(any(), anyLong(), any())) - .willThrow(new RoomEscapeException(ErrorCode.RESERVATION_NOT_FOUND)); - - mockMvc.perform(put("/reservations/999") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()); - } - - @Test - void 예약_수정시_과거_날짜면_422를_반환한다() throws Exception { - ReservationUpdateRequest request = RoomEscapeFixture.reservationUpdateRequestWithPastDate(); - given(reservationService.update(any(), anyLong(), any())) - .willThrow(new RoomEscapeException(ErrorCode.PAST_RESERVATION_NOT_ALLOWED)); - - mockMvc.perform(put("/reservations/1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isUnprocessableEntity()); - } - - @Test - void 대기_예약_조회시_상태가_대기로_반환된다() throws Exception { - given(reservationService.find(2L)).willReturn(RoomEscapeFixture.reservationResultWithWaiting()); - - mockMvc.perform(get("/reservations/2")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.state").value("대기")) - .andExpect(jsonPath("$.rank").value(2)); - } -} diff --git a/src/test/java/roomescape/domain/reservation/RankedReservationTest.java b/src/test/java/roomescape/domain/reservation/RankedReservationTest.java index c23878cb7d..d11e6bd2b2 100644 --- a/src/test/java/roomescape/domain/reservation/RankedReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/RankedReservationTest.java @@ -8,18 +8,18 @@ class RankedReservationTest { - private static final Reservation TODAY = RoomEscapeFixture.reservationWithLocalDateTime( - LocalDateTime.of(2026, 6, 2, 10, 0)); + private static final Reservation TODAY = RoomEscapeFixture.reservation() + .createdAt(LocalDateTime.of(2026, 6, 2, 10, 0)).build(); @Test void 예약과_예약목록으로_순번을_정하여_생성된다() { List reservations = List.of( - RoomEscapeFixture.reservationWithLocalDateTime(LocalDateTime.of(2026, 6, 1, 10, 0)), - RoomEscapeFixture.reservationWithLocalDateTime(LocalDateTime.of(2026, 6, 1, 10, 1)), + RoomEscapeFixture.reservation().createdAt(LocalDateTime.of(2026, 6, 1, 10, 0)).build(), + RoomEscapeFixture.reservation().createdAt(LocalDateTime.of(2026, 6, 1, 10, 1)).build(), TODAY, - RoomEscapeFixture.reservationWithLocalDateTime(LocalDateTime.of(2026, 6, 3, 10, 0)), - RoomEscapeFixture.reservationWithLocalDateTime(LocalDateTime.of(2027, 7, 2, 10, 0))); + RoomEscapeFixture.reservation().createdAt(LocalDateTime.of(2026, 6, 3, 10, 0)).build(), + RoomEscapeFixture.reservation().createdAt(LocalDateTime.of(2027, 7, 2, 10, 0)).build()); Assertions.assertThat(RankedReservation.decideRankFrom(TODAY, reservations).getRank().getValue()).isEqualTo(2); } -} \ No newline at end of file +} diff --git a/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java b/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java index 305450c86a..b861cab58c 100644 --- a/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java +++ b/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java @@ -2,30 +2,20 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.List; import org.junit.jupiter.api.Test; -import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeName; -import roomescape.domain.theme.ThumbnailUrl; +import roomescape.RoomEscapeFixture; class RankedReservationsTest { - private static final Slot SLOT = Slot.load(1L, - new ReservationDate(LocalDate.of(2099, 1, 1)), - ReservationTime.of(1L, LocalTime.of(10, 0)), - Theme.load(1L, new ThemeName("테마"), "설명", new ThumbnailUrl("https://zeze.com"))); - @Test void 같은_슬롯에서_먼저_예약한_사람이_rank_0이다() { - Reservation first = Reservation.load(1L, new ReservationName("제제"), SLOT, Status.APPROVED, - LocalDateTime.of(2099, 1, 1, 9, 0)); - Reservation second = Reservation.load(2L, new ReservationName("달수"), SLOT, Status.WAITING, - LocalDateTime.of(2099, 1, 1, 9, 1)); + Reservation first = RoomEscapeFixture.reservation() + .name("제제").createdAt(LocalDateTime.of(2099, 1, 1, 9, 0)).build(); + Reservation second = RoomEscapeFixture.reservation() + .id(2L).name("달수").status(Status.WAITING).createdAt(LocalDateTime.of(2099, 1, 1, 9, 1)).build(); RankedReservations rankedReservations = new RankedReservations(List.of(first, second)); - List results = rankedReservations.resultsOf(); assertThat(results.get(0).getRank().getValue()).isEqualTo(0); @@ -34,16 +24,15 @@ class RankedReservationsTest { @Test void 이름으로_조회하면_해당_이름의_예약만_반환된다() { - Reservation r1 = Reservation.load(1L, new ReservationName("제제"), SLOT, Status.APPROVED, - LocalDateTime.of(2099, 1, 1, 9, 0)); - Reservation r2 = Reservation.load(2L, new ReservationName("달수"), SLOT, Status.WAITING, - LocalDateTime.of(2099, 1, 1, 9, 1)); + Reservation r1 = RoomEscapeFixture.reservation() + .name("제제").createdAt(LocalDateTime.of(2099, 1, 1, 9, 0)).build(); + Reservation r2 = RoomEscapeFixture.reservation() + .id(2L).name("달수").status(Status.WAITING).createdAt(LocalDateTime.of(2099, 1, 1, 9, 1)).build(); RankedReservations rankedReservations = new RankedReservations(List.of(r1, r2)); - List results = rankedReservations.resultsOf("달수"); assertThat(results).hasSize(1); - assertThat(results.get(0).getReservation()).isEqualTo(r2); + assertThat(results.getFirst().getReservation()).isEqualTo(r2); } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 287b85bb5d..3a6cd27c2b 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -20,8 +20,8 @@ public class ReservationTest { } static Stream nullCases() { - ReservationName name = RoomEscapeFixture.reservationName(); - Slot slot = RoomEscapeFixture.slot(); + ReservationName name = new ReservationName("zeze"); + Slot slot = RoomEscapeFixture.slot().build(); return Stream.of( Arguments.of(null, slot, Status.APPROVED), @@ -32,16 +32,18 @@ static Stream nullCases() { @Test void 과거_예약인지_비교할_수_있다() { - Reservation past = RoomEscapeFixture.reservationWithPast(); - Reservation future = RoomEscapeFixture.reservationWithFuture(); + Reservation past = RoomEscapeFixture.reservation().createdAt(RoomEscapeFixture.PAST_DATE_TIME).build(); + Reservation future = RoomEscapeFixture.reservation().id(2L).createdAt(RoomEscapeFixture.FUTURE_DATE_TIME) + .build(); Assertions.assertThat(past.isEarlierThan(future)).isTrue(); } @Test void 시점이_같을때_id가_더_작으면_false를_반환한다() { - Reservation id1WithSameDate = RoomEscapeFixture.reservationWithApproved(); - Reservation id2WithSameDate = RoomEscapeFixture.reservationWithPast(); + Reservation id1WithSameDate = RoomEscapeFixture.reservation().build(); + Reservation id2WithSameDate = RoomEscapeFixture.reservation().createdAt(RoomEscapeFixture.PAST_DATE_TIME) + .build(); Assertions.assertThat(id1WithSameDate.isEarlierThan(id2WithSameDate)).isFalse(); } diff --git a/src/test/java/roomescape/domain/reservation/SlotTest.java b/src/test/java/roomescape/domain/reservation/SlotTest.java index f6a22ac54a..e5eadeb07c 100644 --- a/src/test/java/roomescape/domain/reservation/SlotTest.java +++ b/src/test/java/roomescape/domain/reservation/SlotTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -18,8 +20,8 @@ public class SlotTest { } static Stream nullCases() { - ReservationDate date = RoomEscapeFixture.reservationDate(); - ReservationTime time = RoomEscapeFixture.reservationTime(); + ReservationDate date = new ReservationDate(LocalDate.now()); + ReservationTime time = ReservationTime.of(1L, LocalTime.now()); Theme theme = RoomEscapeFixture.theme(); return Stream.of( diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index 90638bce57..f838c7684c 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Optional; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -25,7 +27,6 @@ import roomescape.domain.reservation.RankedReservation; 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; @@ -42,17 +43,8 @@ class ReservationServiceTest { private static final String URL = "https://zeze.com/thumb.jpg"; private static final String NAME = "제제"; private static final LocalDateTime TODAY = LocalDateTime.of(2026, 5, 10, 10, 0, 0); - private static final ReservationTime DUMMY_TIME = ReservationTime.of(1L, LocalTime.of(10, 0)); - private static final Theme DUMMY_THEME = Theme.load(1L, new ThemeName("any"), "any", new ThumbnailUrl(URL)); - private static final Slot DUMMY_SLOT = Slot.load(1L, new ReservationDate(LocalDate.of(2099, 1, 1)), - DUMMY_TIME, DUMMY_THEME); - private static final Reservation DUMMY = Reservation.load( - 1L, - new ReservationName(NAME), - DUMMY_SLOT, - Status.APPROVED, - TODAY - ); + private static final Slot DUMMY_SLOT = RoomEscapeFixture.slot().build(); + private static final Reservation DUMMY = RoomEscapeFixture.reservation().name(NAME).createdAt(TODAY).build(); private static final long NOT_EXISTS_ID = Long.MAX_VALUE; private static final long EXISTS_ID = 1L; @@ -71,281 +63,196 @@ class ReservationServiceTest { @InjectMocks private ReservationService reservationService; - - @Test - void 예약_취소_성공() { - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); - given(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(anyLong(), anyLong(), any())) - .willReturn(Optional.empty()); - - reservationService.cancel(1L, LocalDateTime.MIN); - - verify(reservationRepository).deleteById(1L); - } - - @Test - void APPROVED_예약_취소_시_첫번째_WAITING_예약이_승격된다() { - Reservation waiting = Reservation.load(2L, new ReservationName("대기자"), DUMMY_SLOT, - Status.WAITING, TODAY); - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); - given(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(anyLong(), anyLong(), any())) - .willReturn(Optional.of(waiting)); - - reservationService.cancel(1L, LocalDateTime.MIN); - - verify(reservationRepository).updateStatus(2L, Status.APPROVED); - } - - @Test - void WAITING_예약_취소_시_승격이_발생하지_않는다() { - Reservation waiting = Reservation.load(2L, new ReservationName("대기자"), DUMMY_SLOT, - Status.WAITING, TODAY); - given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); - - reservationService.cancel(2L, LocalDateTime.MIN); - - verify(reservationRepository).deleteById(2L); - org.mockito.Mockito.verifyNoMoreInteractions(reservationRepository); - } - - @Test - void 존재하지_않는_예약_취소시_예외_발생() { - given(reservationRepository.findById(999L)).willReturn(Optional.empty()); - - Assertions.assertThatThrownBy(() -> reservationService.cancel(999L, LocalDateTime.MIN)) - .isInstanceOf(RoomEscapeException.class); + @Nested + @DisplayName("reserve") + class Reserve { + + @Test + void 존재하지_않는_시간으로_예약시_예외가_발생한다() { + given(reservationTimeRepository.findById(999L)).willReturn(Optional.empty()); + + ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequest(); + + Assertions.assertThatThrownBy(() -> reservationService.reserve(request, LocalDateTime.MAX)) + .isInstanceOf(RoomEscapeException.class); + } + + @Test + void APPROVED가_이미_있으면_WAITING으로_예약된다() { + ReservationTime reservationTime = ReservationTime.of(LocalTime.parse("11:00")); + Theme theme = Theme.load(1L, new ThemeName("테마1"), "설명", new ThumbnailUrl(URL)); + Slot waitingSlot = RoomEscapeFixture.slot().id(2L) + .date(new ReservationDate(LocalDate.parse("2026-04-05"))).time(reservationTime).theme(theme) + .build(); + Reservation waitingSaved = RoomEscapeFixture.reservation().id(2L).name("zeze").slot(waitingSlot) + .status(Status.WAITING).createdAt(TODAY).build(); + + ReservationCreateRequest request = new ReservationCreateRequest("zeze", + LocalDate.parse("2026-04-05"), 1L, 1L); + given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); + given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); + given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())) + .willReturn(true); + given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())) + .willReturn(Optional.of(waitingSlot)); + given(reservationRepository.save(any())).willReturn(waitingSaved); + given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())) + .willReturn(List.of(DUMMY, waitingSaved)); + + RankedReservation result = reservationService.reserve(request, + LocalDateTime.of(2026, 4, 5, 10, 59, 59)); + + Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.WAITING); + } } - @Test - void 존재하지_않는_시간으로_예약시_예외() { - given(reservationTimeRepository.findById(999L)).willReturn(Optional.empty()); + @Nested + @DisplayName("find") + class Find { - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.parse("2026-05-03"), 999L, - 1L); + @Test + void 존재하는_ID면_결과를_반환한다() { + given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); + given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); - Assertions.assertThatThrownBy(() -> reservationService.reserve(request, LocalDateTime.MAX)) - .isInstanceOf(RoomEscapeException.class); - } - - @Test - void 지나간_날짜로_예약_시_예외가_발생해야_한다() { - ReservationTime reservationTime = ReservationTime.of(LocalTime.parse("11:00")); - Theme theme = Theme.load(1L, new ThemeName("테마1"), "설명", new ThumbnailUrl(URL)); - - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.parse("2026-04-05"), 1L, 1L); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); - given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); - given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(false); - given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( - Optional.of(DUMMY_SLOT)); - - Assertions.assertThatThrownBy(() -> reservationService.reserve(request, LocalDateTime.MAX)); - } - - @Test - void 같은_날짜이며_시간이_1초_전이면_예약에_성공해야_한다() { - ReservationTime reservationTime = ReservationTime.of(LocalTime.parse("11:00")); - Theme theme = Theme.load(1L, new ThemeName("테마1"), "설명", new ThumbnailUrl(URL)); - - ReservationCreateRequest request = new ReservationCreateRequest(NAME, LocalDate.of(2026, 4, 5), 1L, 1L); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); - given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); - given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(false); - given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( - Optional.of(DUMMY_SLOT)); - given(reservationRepository.save(any())).willReturn(DUMMY); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); - - Assertions.assertThatNoException() - .isThrownBy(() -> reservationService.reserve(request, LocalDateTime.of(2026, 4, 5, 10, 59, 59))); - } + RankedReservation result = reservationService.find(EXISTS_ID); - @Test - void 같은_날짜이며_시간이_1초_지났다면_예약에_실패해야_한다() { - ReservationTime reservationTime = ReservationTime.of(LocalTime.parse("11:00")); - Theme theme = Theme.load(1L, new ThemeName("테마1"), "설명", new ThumbnailUrl(URL)); + Assertions.assertThat(result.getReservation().getId()).isEqualTo(EXISTS_ID); + Assertions.assertThat(result.getRank().getValue()).isZero(); + } - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.parse("2026-04-05"), 1L, 1L); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); - given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); - given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(false); - given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( - Optional.of(DUMMY_SLOT)); + @Test + void 존재하지_않는_ID면_예외가_발생한다() { + given(reservationRepository.findById(NOT_EXISTS_ID)).willReturn(Optional.empty()); - Assertions.assertThatThrownBy( - () -> reservationService.reserve(request, LocalDateTime.of(2026, 4, 5, 11, 0, 1))); + Assertions.assertThatThrownBy(() -> reservationService.find(NOT_EXISTS_ID)) + .isInstanceOf(RoomEscapeException.class) + .hasMessage(ErrorCode.RESERVATION_NOT_FOUND.getMessage()); + } } - @Test - void 미래로_예약하면_성공해야_한다() { - ReservationTime reservationTime = ReservationTime.of(LocalTime.parse("11:00")); - Theme theme = Theme.load(1L, new ThemeName("테마1"), "설명", new ThumbnailUrl(URL)); - - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.parse("2026-04-05"), 1L, 1L); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); - given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); - given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(false); - given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( - Optional.of(DUMMY_SLOT)); - given(reservationRepository.save(any())).willReturn(DUMMY); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); - - Assertions.assertThatNoException().isThrownBy( - () -> reservationService.reserve(request, LocalDateTime.of(2026, 4, 5, 10, 59, 59))); - } + @Nested + @DisplayName("findList") + class FindList { - @Test - void APPROVED가_이미_있으면_WAITING으로_예약된다() { - ReservationTime reservationTime = ReservationTime.of(LocalTime.parse("11:00")); - Theme theme = Theme.load(1L, new ThemeName("테마1"), "설명", new ThumbnailUrl(URL)); - Slot waitingSlot = Slot.load(2L, new ReservationDate(LocalDate.parse("2026-04-05")), reservationTime, theme); - Reservation waitingSaved = Reservation.load(2L, new ReservationName("zeze"), waitingSlot, - Status.WAITING, TODAY); - - ReservationCreateRequest request = new ReservationCreateRequest("zeze", LocalDate.parse("2026-04-05"), 1L, 1L); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); - given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); - given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())).willReturn(true); - given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())).willReturn( - Optional.of(waitingSlot)); - given(reservationRepository.save(any())).willReturn(waitingSaved); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn( - List.of(DUMMY, waitingSaved)); - - RankedReservation result = reservationService.reserve(request, LocalDateTime.of(2026, 4, 5, 10, 59, 59)); - - Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.WAITING); - } + @Test + void 이름_없이_목록_조회시_전체_예약을_반환한다() { + given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); - @Test - void 예약_수정시_ID가_없으면_예외가_발생한다() { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.parse("2099-04-06"), 1L, - 1L); - given(reservationRepository.findById(999L)).willReturn(Optional.empty()); + List results = reservationService.findList(null); - Assertions.assertThatThrownBy(() -> reservationService.update(request, 999L, LocalDateTime.MIN)) - .isInstanceOf(RoomEscapeException.class).hasMessage( - ErrorCode.RESERVATION_NOT_FOUND.getMessage()); - } + Assertions.assertThat(results).hasSize(1); + } - @Test - void 예약_수정시_과거_날짜의_예약이면_예외가_발생한다() { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.parse("2000-04-06"), 1L, - 1L); - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); + @Test + void 이름으로_목록_조회시_해당_이름의_예약만_반환한다() { + given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); - Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MAX)) - .isInstanceOf(RoomEscapeException.class).hasMessage( - ErrorCode.PAST_RESERVATION_NOT_ALLOWED.getMessage()); - } + List results = reservationService.findList(NAME); - @Test - void 예약_수정시_시간을_찾을_수_없으면_예외가_발생한다() { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.parse("2099-04-06"), 1L, - 1L); - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.empty()); - - Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MIN)) - .isInstanceOf(RoomEscapeException.class).hasMessage( - ErrorCode.RESERVATION_TIME_NOT_FOUND.getMessage()); + Assertions.assertThat(results).hasSize(1); + Assertions.assertThat(results.getFirst().getReservation().getName().getValue()).isEqualTo(NAME); + } } - @Test - void 예약_수정시_사용_불가능한_날짜가_들어오면_예외가_발생한다() { - ReservationTime reservationTime = ReservationTime.of(1L, LocalTime.parse("11:00")); - - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.parse("2099-04-06"), 1L, - 1L); - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); - given(reservationRepository.existsByTimeAndThemeAndDateAndName(request.getTimeId(), request.getThemeId(), - request.getDate(), request.getName())).willReturn(true); - - Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MIN)) - .isInstanceOf(RoomEscapeException.class).hasMessage( - ErrorCode.DUPLICATE_RESERVATION.getMessage()); + @Nested + @DisplayName("update") + class Update { + + @Test + void ID가_없으면_예외가_발생한다() { + ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", + LocalDate.parse("2099-04-06"), 1L, 1L); + given(reservationRepository.findById(999L)).willReturn(Optional.empty()); + + Assertions.assertThatThrownBy(() -> reservationService.update(request, 999L, LocalDateTime.MIN)) + .isInstanceOf(RoomEscapeException.class); + } + + @Test + void 과거_날짜의_예약이면_예외가_발생한다() { + ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", + LocalDate.parse("2000-04-06"), 1L, 1L); + given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); + + Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MAX)) + .isInstanceOf(RoomEscapeException.class); + } + + @Test + void 시간을_찾을_수_없으면_예외가_발생한다() { + ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", + LocalDate.parse("2099-04-06"), 1L, 1L); + given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); + given(reservationTimeRepository.findById(1L)).willReturn(Optional.empty()); + + Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MIN)) + .isInstanceOf(RoomEscapeException.class) + .hasMessage(ErrorCode.RESERVATION_TIME_NOT_FOUND.getMessage()); + } + + @Test + void 이름_중복이면_예외가_발생한다() { + ReservationTime reservationTime = ReservationTime.of(1L, LocalTime.parse("11:00")); + ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", + LocalDate.parse("2099-04-06"), 1L, 1L); + given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); + given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); + given(reservationRepository.existsByTimeAndThemeAndDateAndName(request.getTimeId(), + request.getThemeId(), request.getDate(), request.getName())).willReturn(true); + + Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MIN)) + .isInstanceOf(RoomEscapeException.class) + .hasMessage(ErrorCode.DUPLICATE_RESERVATION.getMessage()); + } } - @Test - void 예약_삭제_시_ID가_존재하지_않으면_예외가_발생한다() { - given(reservationRepository.findById(NOT_EXISTS_ID)).willThrow(RoomEscapeException.class); - - Assertions.assertThatThrownBy(() -> reservationRepository.findById(NOT_EXISTS_ID)) - .isInstanceOf(RoomEscapeException.class); - } - - @Test - void 예약_삭제_시_문제가_없으면_삭제되어야_한다() { - Reservation reservation = RoomEscapeFixture.reservationWithApproved(); - given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(reservation)); - given(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(anyLong(), anyLong(), any())) - .willReturn(Optional.empty()); - - assertThatCode(() -> reservationService.cancel(EXISTS_ID, TODAY)).doesNotThrowAnyException(); - } - - @Test - void 단건_조회시_존재하는_ID면_결과를_반환한다() { - given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); - - RankedReservation result = reservationService.find(EXISTS_ID); - - Assertions.assertThat(result.getReservation().getId()).isEqualTo(EXISTS_ID); - Assertions.assertThat(result.getRank().getValue()).isEqualTo(0); - } - - @Test - void 단건_조회시_존재하지_않는_ID면_예외가_발생한다() { - given(reservationRepository.findById(NOT_EXISTS_ID)).willReturn(Optional.empty()); - - Assertions.assertThatThrownBy(() -> reservationService.find(NOT_EXISTS_ID)) - .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorCode.RESERVATION_NOT_FOUND.getMessage()); - } - - @Test - void 이름_없이_목록_조회시_전체_예약을_반환한다() { - given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); - - List results = reservationService.findList(null); - - Assertions.assertThat(results).hasSize(1); - Assertions.assertThat(results.get(0).getReservation().getId()).isEqualTo(EXISTS_ID); - } - - @Test - void 이름으로_목록_조회시_해당_이름의_예약만_반환한다() { - given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); - - List results = reservationService.findList(NAME); - - Assertions.assertThat(results).hasSize(1); - Assertions.assertThat(results.get(0).getReservation().getName().getValue()).isEqualTo(NAME); - } - - @Test - void 첫번째_예약은_승인_상태이다() { - given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); - - RankedReservation result = reservationService.find(EXISTS_ID); - - Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.APPROVED); - } - - @Test - void 두번째_이후_예약은_대기_상태이다() { - Reservation waiting = Reservation.load(2L, new ReservationName("대기자"), DUMMY_SLOT, - Status.WAITING, TODAY); - given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())) - .willReturn(List.of(DUMMY, waiting)); - - RankedReservation result = reservationService.find(2L); - - Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.WAITING); - Assertions.assertThat(result.getRank().getValue()).isEqualTo(1); + @Nested + @DisplayName("cancel") + class Cancel { + + @Test + void 정상_취소시_삭제된다() { + given(reservationRepository.findById(EXISTS_ID)) + .willReturn(Optional.of(RoomEscapeFixture.reservation().build())); + given(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(anyLong(), anyLong(), any())) + .willReturn(Optional.empty()); + + assertThatCode(() -> reservationService.cancel(EXISTS_ID, TODAY)).doesNotThrowAnyException(); + verify(reservationRepository).deleteById(EXISTS_ID); + } + + @Test + void APPROVED_예약_취소_시_첫번째_WAITING_예약이_승격된다() { + Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자").slot(DUMMY_SLOT) + .status(Status.WAITING).createdAt(TODAY).build(); + given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); + given(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(anyLong(), anyLong(), any())) + .willReturn(Optional.of(waiting)); + + reservationService.cancel(1L, LocalDateTime.MIN); + + verify(reservationRepository).updateStatus(2L, Status.APPROVED); + } + + @Test + void WAITING_예약_취소_시_승격이_발생하지_않는다() { + Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자").slot(DUMMY_SLOT) + .status(Status.WAITING).createdAt(TODAY).build(); + given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); + + reservationService.cancel(2L, LocalDateTime.MIN); + + verify(reservationRepository).deleteById(2L); + org.mockito.Mockito.verifyNoMoreInteractions(reservationRepository); + } + + @Test + void 존재하지_않는_예약_취소시_예외_발생() { + given(reservationRepository.findById(999L)).willReturn(Optional.empty()); + + Assertions.assertThatThrownBy(() -> reservationService.cancel(999L, LocalDateTime.MIN)) + .isInstanceOf(RoomEscapeException.class); + } } } From 6c734edc9ff6ef6e29e5c497fc3ff7ddc02a40fe Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Wed, 3 Jun 2026 20:05:16 +0900 Subject: [PATCH 06/26] =?UTF-8?q?refactor:=20repository=20save=EC=8B=9C=20?= =?UTF-8?q?withId=20=ED=98=95=ED=83=9C=EB=A1=9C=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/domain/reservation/Reservation.java | 4 ++++ .../domain/reservation/ReservationTime.java | 4 ++++ src/main/java/roomescape/domain/theme/Theme.java | 4 ++++ .../roomescape/repository/ReservationRepository.java | 12 +++++------- .../repository/ReservationTimeRepository.java | 2 +- .../java/roomescape/repository/ThemeRepository.java | 2 +- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 26ce6a9092..866967fe2d 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -53,6 +53,10 @@ public boolean isSameSlot(Reservation target) { return slot.isSame(target); } + public Reservation withId(long id) { + return new Reservation(id, name, slot, status, createdAt); + } + public long getId() { return id; } diff --git a/src/main/java/roomescape/domain/reservation/ReservationTime.java b/src/main/java/roomescape/domain/reservation/ReservationTime.java index a1487236f8..e0fbf3ee69 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservation/ReservationTime.java @@ -20,6 +20,10 @@ public static ReservationTime of(LocalTime startAt) { return new ReservationTime(0L, startAt); } + public ReservationTime withId(long id) { + return new ReservationTime(id, startAt); + } + public long getId() { return id; } diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index 662fd94197..8dbd21f12b 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -23,6 +23,10 @@ public static Theme create(ThemeName name, String description, ThumbnailUrl thum return new Theme(0L, name, description, thumbnailUrl); } + public Theme withId(long id) { + return new Theme(id, name, description, thumbnailUrl); + } + public long getId() { return id; } diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index 12c36efa77..f8d2b99553 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -115,16 +115,14 @@ public Reservation save(Reservation reservation) { long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return Reservation.load(generatedKey, reservation.getName(), reservation.getSlot(), - reservation.getStatus(), reservation.getCreatedAt()); + return reservation.withId(generatedKey); } - public Reservation update(long id, Reservation target) { - jdbcTemplate.update(UPDATE, target.getName().getValue(), target.getSlot().getId(), - target.getCreatedAt(), id); + public Reservation update(long id, Reservation reservation) { + jdbcTemplate.update(UPDATE, reservation.getName().getValue(), reservation.getSlot().getId(), + reservation.getCreatedAt(), id); - return Reservation.load(id, target.getName(), target.getSlot(), - target.getStatus(), target.getCreatedAt()); + return reservation.withId(id); } public void deleteById(Long id) { diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java index 5bb1f042ff..ee495b039b 100644 --- a/src/main/java/roomescape/repository/ReservationTimeRepository.java +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -38,7 +38,7 @@ public ReservationTime save(ReservationTime time) { long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return ReservationTime.of(generatedKey, time.getStartAt()); + return time.withId(generatedKey); } public List findAll() { diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java index 55987320d5..13ef27314d 100644 --- a/src/main/java/roomescape/repository/ThemeRepository.java +++ b/src/main/java/roomescape/repository/ThemeRepository.java @@ -42,7 +42,7 @@ public Theme save(Theme theme) { "thumbnail_url", theme.getThumbnailUrl().getValue() ); long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return Theme.load(generatedKey, theme.getName(), theme.getDescription(), theme.getThumbnailUrl()); + return theme.withId(generatedKey); } public List findAll() { From 258371edd3e5056c735b06407b8057371e6748dc Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Wed, 3 Jun 2026 20:06:06 +0900 Subject: [PATCH 07/26] =?UTF-8?q?refactor:=20Rank=20=EC=82=B0=EC=A0=95=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReservationService.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 07881bcd55..90da48db00 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -10,14 +10,12 @@ import org.springframework.transaction.annotation.Transactional; import roomescape.controller.dto.request.ReservationCreateRequest; import roomescape.controller.dto.request.ReservationUpdateRequest; -import roomescape.domain.reservation.Rank; import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.RankedReservations; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationName; 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; @@ -66,12 +64,13 @@ public RankedReservation reserve(ReservationCreateRequest request, LocalDateTime Reservation reservation = Reservation.reserve(new ReservationName(request.getName()), slot, status, now); Reservation saved = reservationRepository.save(reservation); - Reservations reservations = new Reservations(reservationRepository.findByTimeAndThemeAndDate( - saved.getTime(), saved.getTheme(), saved.getDate())); - - Rank rank = reservations.rankOf(saved); + return getRankedReservation(saved); + } - return new RankedReservation(rank, saved); + private RankedReservation getRankedReservation(Reservation target) { + List reservations = reservationRepository.findByTimeAndThemeAndDate( + target.getTime(), target.getTheme(), target.getDate()); + return RankedReservation.decideRankFrom(target, reservations); } public RankedReservation find(long reservationId) { @@ -118,12 +117,7 @@ public RankedReservation update(ReservationUpdateRequest request, long id, Local originReservation.getDate().getValue() ).ifPresent(waiting -> reservationRepository.updateStatus(waiting.getId(), Status.APPROVED)); - Reservations reservations = new Reservations(reservationRepository.findByTimeAndThemeAndDate( - updated.getTime(), updated.getTheme(), updated.getDate())); - - Rank rank = reservations.rankOf(updated); - - return new RankedReservation(rank, updated); + return getRankedReservation(updated); } @Transactional From 9c3113e0565b5d5621674b07b0cfabb5334dc75c Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 4 Jun 2026 10:55:16 +0900 Subject: [PATCH 08/26] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20public=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=EC=9E=90=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/Reservations.java | 30 ------------------- .../service/ReservationService.java | 1 + .../java/roomescape/MissionStep2Test.java | 2 +- .../java/roomescape/MissionStep3Test.java | 4 +-- .../java/roomescape/MissionStep4Test.java | 2 +- .../roomescape/RoomescapeApplicationTest.java | 9 ------ .../reservation/ReservationDateTest.java | 2 +- .../reservation/ReservationNameTest.java | 2 +- .../domain/reservation/ReservationTest.java | 2 +- .../reservation/ReservationTimeTest.java | 2 +- .../domain/reservation/SlotTest.java | 2 +- .../domain/theme/ThemeNameTest.java | 2 +- .../roomescape/domain/theme/ThemeTest.java | 2 +- .../repository/ReservationRepositoryTest.java | 2 +- .../ReservationServiceIntegrationTest.java | 2 -- .../service/ReservationServiceTest.java | 2 +- .../service/ReservationTimeServiceTest.java | 2 +- .../roomescape/service/ThemeServiceTest.java | 2 +- 18 files changed, 16 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/roomescape/domain/reservation/Reservations.java diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java deleted file mode 100644 index bd7886406b..0000000000 --- a/src/main/java/roomescape/domain/reservation/Reservations.java +++ /dev/null @@ -1,30 +0,0 @@ -package roomescape.domain.reservation; - -import java.util.List; - -public class Reservations { - private final List reservations; - - public Reservations(List reservations) { - this.reservations = reservations; - } - - public Rank rankOf(Reservation target) { - long earlierCount = reservations.stream() - .filter(r -> isEarlierThan(r, target)) - .count(); - return new Rank((int) earlierCount); - } - - private boolean isEarlierThan(Reservation source, Reservation target) { - int byTime = source.getCreatedAt().compareTo(target.getCreatedAt()); - if (byTime != 0) { - return byTime < 0; - } - return source.getId() < target.getId(); - } - - public List getReservations() { - return reservations; - } -} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 90da48db00..a7955332c4 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -25,6 +25,7 @@ import roomescape.repository.ThemeRepository; @Service +@Transactional(readOnly = true) public class ReservationService { private final ReservationRepository reservationRepository; private final ReservationTimeRepository reservationTimeRepository; diff --git a/src/test/java/roomescape/MissionStep2Test.java b/src/test/java/roomescape/MissionStep2Test.java index ed3e9ce396..de0a65207a 100644 --- a/src/test/java/roomescape/MissionStep2Test.java +++ b/src/test/java/roomescape/MissionStep2Test.java @@ -23,7 +23,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class MissionStep2Test { +class MissionStep2Test { @Autowired private JdbcTemplate jdbcTemplate; diff --git a/src/test/java/roomescape/MissionStep3Test.java b/src/test/java/roomescape/MissionStep3Test.java index f2bb0ca56a..b00dbf7e98 100644 --- a/src/test/java/roomescape/MissionStep3Test.java +++ b/src/test/java/roomescape/MissionStep3Test.java @@ -17,10 +17,10 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class MissionStep3Test { +class MissionStep3Test { @Autowired private JdbcTemplate jdbcTemplate; - + @LocalServerPort int port; diff --git a/src/test/java/roomescape/MissionStep4Test.java b/src/test/java/roomescape/MissionStep4Test.java index 01bdcd9d00..fb4715ab59 100644 --- a/src/test/java/roomescape/MissionStep4Test.java +++ b/src/test/java/roomescape/MissionStep4Test.java @@ -16,7 +16,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class MissionStep4Test { +class MissionStep4Test { @Autowired private ReservationController reservationController; diff --git a/src/test/java/roomescape/RoomescapeApplicationTest.java b/src/test/java/roomescape/RoomescapeApplicationTest.java index 1e6f8643d1..9b66e38c5a 100644 --- a/src/test/java/roomescape/RoomescapeApplicationTest.java +++ b/src/test/java/roomescape/RoomescapeApplicationTest.java @@ -68,15 +68,6 @@ void init() { assertThat(after).isEqualTo(before); } - @Test - void 과거_날짜로_사용_시간_조회시_400을_반환한다() { - String past = "2020-01-01"; - - RestAssured.given() - .when().get("/times/available?date=" + past + "&themeId=1") - .then().statusCode(422); - } - @Test void themeId_없이_사용_시간_조회시_400을_반환한다() { RestAssured.given() diff --git a/src/test/java/roomescape/domain/reservation/ReservationDateTest.java b/src/test/java/roomescape/domain/reservation/ReservationDateTest.java index 62236f6fe6..04770776a2 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationDateTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationDateTest.java @@ -4,7 +4,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -public class ReservationDateTest { +class ReservationDateTest { @Test void null을_입력받으면_예외가_발생한다() { Assertions.assertThatThrownBy(() -> new ReservationDate(null)).isInstanceOf(NullPointerException.class); diff --git a/src/test/java/roomescape/domain/reservation/ReservationNameTest.java b/src/test/java/roomescape/domain/reservation/ReservationNameTest.java index b2fdd8311e..8feae08620 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationNameTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationNameTest.java @@ -8,7 +8,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -public class ReservationNameTest { +class ReservationNameTest { private static final String UNDER_SIZE_NAME = ""; private static final String OVER_SIZE_NAME = "---------------------"; diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 3a6cd27c2b..2cf1cda19d 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.params.provider.MethodSource; import roomescape.RoomEscapeFixture; -public class ReservationTest { +class ReservationTest { @ParameterizedTest @MethodSource("nullCases") void 매개변수에_NULL이_포함되면_예외가_발생한다(ReservationName reservationName, Slot slot, Status status) { diff --git a/src/test/java/roomescape/domain/reservation/ReservationTimeTest.java b/src/test/java/roomescape/domain/reservation/ReservationTimeTest.java index 380df54f08..c8c0636b44 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTimeTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTimeTest.java @@ -4,7 +4,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -public class ReservationTimeTest { +class ReservationTimeTest { @Test void null을_입력받으면_예외가_발생한다() { Assertions.assertThatThrownBy(() -> ReservationTime.of(1, null)) diff --git a/src/test/java/roomescape/domain/reservation/SlotTest.java b/src/test/java/roomescape/domain/reservation/SlotTest.java index e5eadeb07c..d67550ad03 100644 --- a/src/test/java/roomescape/domain/reservation/SlotTest.java +++ b/src/test/java/roomescape/domain/reservation/SlotTest.java @@ -11,7 +11,7 @@ import roomescape.RoomEscapeFixture; import roomescape.domain.theme.Theme; -public class SlotTest { +class SlotTest { @ParameterizedTest @MethodSource("nullCases") void 매개변수에_NULL이_포함되면_예외가_발생한다(ReservationDate date, ReservationTime time, Theme theme) { diff --git a/src/test/java/roomescape/domain/theme/ThemeNameTest.java b/src/test/java/roomescape/domain/theme/ThemeNameTest.java index 9c22b9bf89..ec72a10c2f 100644 --- a/src/test/java/roomescape/domain/theme/ThemeNameTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeNameTest.java @@ -4,7 +4,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -public class ThemeNameTest { +class ThemeNameTest { private static final String UNDER_SIZE_NAME = ""; private static final String OVER_SIZE_NAME = "-------------------------------"; diff --git a/src/test/java/roomescape/domain/theme/ThemeTest.java b/src/test/java/roomescape/domain/theme/ThemeTest.java index 3f6d4f96ec..e3c4fa6993 100644 --- a/src/test/java/roomescape/domain/theme/ThemeTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -public class ThemeTest { +class ThemeTest { @ParameterizedTest @MethodSource("nullCases") void 매개변수에_NULL이_포함되면_예외가_발생한다(ThemeName themeName, String description, ThumbnailUrl thumbnailUrl) { diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index 0e0ad7df30..f52077f71c 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -34,7 +34,7 @@ SlotRepository.class }) class ReservationRepositoryTest { - private final static Clock FIXED_CLOCK = Clock.fixed( + private static final Clock FIXED_CLOCK = Clock.fixed( Instant.parse("2026-05-10T03:00:00Z"), ZoneId.of("Asia/Seoul") ); diff --git a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java index 7e0780d6c5..a84930fe8e 100644 --- a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java +++ b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java @@ -13,7 +13,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; import roomescape.RoomEscapeFixture; @@ -69,7 +68,6 @@ void init() { if (result.getReservation().getStatus() == Status.WAITING) { waiting.incrementAndGet(); } - } catch (DuplicateKeyException ignored) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index f838c7684c..42ce9a6217 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -69,7 +69,7 @@ class Reserve { @Test void 존재하지_않는_시간으로_예약시_예외가_발생한다() { - given(reservationTimeRepository.findById(999L)).willReturn(Optional.empty()); + given(reservationTimeRepository.findById(1L)).willReturn(Optional.empty()); ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequest(); diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java index 8131e6b9bb..125def282d 100644 --- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java @@ -15,7 +15,7 @@ import roomescape.repository.ReservationTimeRepository; @ExtendWith(MockitoExtension.class) -public class ReservationTimeServiceTest { +class ReservationTimeServiceTest { @Mock private ReservationTimeRepository reservationTimeRepository; @Mock diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java index 3166fefd7f..753ae8dc7e 100644 --- a/src/test/java/roomescape/service/ThemeServiceTest.java +++ b/src/test/java/roomescape/service/ThemeServiceTest.java @@ -19,7 +19,7 @@ import roomescape.repository.ThemeRepository; @ExtendWith(MockitoExtension.class) -public class ThemeServiceTest { +class ThemeServiceTest { @Mock private ThemeRepository themeRepository; From 538d01ec6f92605d8b090acc83922350e34b0ccd Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 4 Jun 2026 10:58:53 +0900 Subject: [PATCH 09/26] =?UTF-8?q?refactor:=20Reservation=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20create=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 --- src/main/java/roomescape/domain/reservation/Reservation.java | 2 +- src/main/java/roomescape/service/ReservationService.java | 4 ++-- .../java/roomescape/domain/reservation/ReservationTest.java | 2 +- .../java/roomescape/repository/ReservationRepositoryTest.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 866967fe2d..23efa69e62 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -26,7 +26,7 @@ public static Reservation load(long id, ReservationName reservationName, Slot sl return new Reservation(id, reservationName, slot, status, createdAt); } - public static Reservation reserve(ReservationName reservationName, Slot slot, Status status, LocalDateTime now) { + public static Reservation create(ReservationName reservationName, Slot slot, Status status, LocalDateTime now) { Objects.requireNonNull(now); Reservation reservation = new Reservation(0L, reservationName, slot, status, now); reservation.ensureNotPast(now); diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index a7955332c4..623747376d 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -62,7 +62,7 @@ public RankedReservation reserve(ReservationCreateRequest request, LocalDateTime status = Status.WAITING; } - Reservation reservation = Reservation.reserve(new ReservationName(request.getName()), slot, status, now); + Reservation reservation = Reservation.create(new ReservationName(request.getName()), slot, status, now); Reservation saved = reservationRepository.save(reservation); return getRankedReservation(saved); @@ -108,7 +108,7 @@ public RankedReservation update(ReservationUpdateRequest request, long id, Local originReservation.getTheme()); Status status = determineStatus(request.getTimeId(), request.getThemeId(), request.getDate()); - Reservation target = Reservation.reserve(originReservation.getName(), slotToUpdate, status, now); + Reservation target = Reservation.create(originReservation.getName(), slotToUpdate, status, now); Reservation updated = reservationRepository.update(id, target); diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 2cf1cda19d..337150ed53 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -15,7 +15,7 @@ class ReservationTest { @ParameterizedTest @MethodSource("nullCases") void 매개변수에_NULL이_포함되면_예외가_발생한다(ReservationName reservationName, Slot slot, Status status) { - assertThatThrownBy(() -> Reservation.reserve(reservationName, slot, status, LocalDateTime.MIN)) + assertThatThrownBy(() -> Reservation.create(reservationName, slot, status, LocalDateTime.MIN)) .isInstanceOf(NullPointerException.class); } diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index f52077f71c..1b679a4043 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -75,7 +75,7 @@ private Reservation reservation(String name, LocalDate date, ReservationTime tim private Reservation reservation(String name, LocalDate date, ReservationTime time, Theme theme, Status status) { Slot slot = giveSlot(date, time, theme); - return Reservation.reserve(new ReservationName(name), slot, status, LocalDateTime.now(FIXED_CLOCK)); + return Reservation.create(new ReservationName(name), slot, status, LocalDateTime.now(FIXED_CLOCK)); } @Nested From 47d4828b50c13884815101f53b09ea808dae6d09 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 4 Jun 2026 16:05:49 +0900 Subject: [PATCH 10/26] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- src/main/java/common/exception/ErrorCode.java | 1 + .../domain/reservation/Reservations.java | 38 ++++ .../roomescape/domain/reservation/Slot.java | 14 ++ .../repository/ReservationRepository.java | 67 ++---- .../repository/ReservationTimeRepository.java | 14 +- .../roomescape/repository/SlotRepository.java | 18 +- .../repository/ThemeRepository.java | 14 +- .../service/ReservationService.java | 118 +++------- .../java/roomescape/service/SlotService.java | 73 +++++++ src/test/java/roomescape/MissionStepTest.java | 2 +- .../domain/reservation/ReservationsTest.java | 65 ++++++ .../domain/theme/ThumbnailUrlTest.java | 2 +- .../repository/ReservationRepositoryTest.java | 202 +++++++++--------- .../repository/SlotRepositoryTest.java | 30 +-- .../ReservationServiceIntegrationTest.java | 2 +- .../service/ReservationServiceTest.java | 149 +++++-------- .../roomescape/service/ThemeServiceTest.java | 6 - 18 files changed, 433 insertions(+), 388 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservation/Reservations.java create mode 100644 src/main/java/roomescape/service/SlotService.java create mode 100644 src/test/java/roomescape/domain/reservation/ReservationsTest.java diff --git a/README.md b/README.md index f31e257e05..7206b2a55d 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ docker-compose up - [x] 같은 슬롯에 대한 대기는 신청 순서대로 순번이 부여된다. - [x] 같은 사용자가 같은 슬롯에 중복 대기할 수 없다. - [x] 사용자는 본인의 대기를 취소할 수 있다. -- [ ] 예약 취소 시 대기 중인 예약이 자동으로 승인으로 변경된다. -- [ ] 대기가 예약으로 전환되면 해당 슬롯의 나머지 대기 순번이 재정렬된다. -- [ ] 예약이 취소되면 해당 슬롯의 대기 순번이 재정렬된다. +- [x] 예약 취소 시 대기 중인 예약이 자동으로 승인으로 변경된다. +- [x] 대기가 예약으로 전환되면 해당 슬롯의 나머지 대기 순번이 재정렬된다. +- [x] 예약이 취소되면 해당 슬롯의 대기 순번이 재정렬된다. ### 예약 조회 diff --git a/src/main/java/common/exception/ErrorCode.java b/src/main/java/common/exception/ErrorCode.java index cac3c2c0e7..09a2cfb795 100644 --- a/src/main/java/common/exception/ErrorCode.java +++ b/src/main/java/common/exception/ErrorCode.java @@ -12,6 +12,7 @@ public enum ErrorCode { THEME_NOT_FOUND("존재하지 않는 테마입니다. 입력을 확인해 주세요.", HttpStatus.NOT_FOUND), RESERVATION_NOT_FOUND("존재하지 않는 예약입니다. 입력을 확인해 주세요.", HttpStatus.NOT_FOUND), RESERVATION_TIME_NOT_FOUND("존재하지 않는 시간입니다. 입력을 확인해 주세요.", HttpStatus.NOT_FOUND), + SLOT_NOT_FOUND("존재하지 않는 슬롯입니다. 입력을 확인해 주세요", HttpStatus.NOT_FOUND), // DB 제약 조건 관련 위반 (CONFLICT) DUPLICATE_RESERVATION("이미 예약된 시간입니다. 다른 시간을 선택해 주세요.", HttpStatus.CONFLICT), diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java new file mode 100644 index 0000000000..37301d42ed --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/Reservations.java @@ -0,0 +1,38 @@ +package roomescape.domain.reservation; + +import common.exception.ErrorCode; +import common.exception.RoomEscapeException; +import java.time.LocalDateTime; +import java.util.List; + +public class Reservations { + private final List reservations; + + public Reservations(List reservations) { + this.reservations = reservations; + } + + public Reservation reserve(ReservationName reservationName, Slot foundSlot, LocalDateTime now) { + validateHasName(reservationName, foundSlot); + + Status status = getStatus(); + + return Reservation.create(reservationName, foundSlot, status, now); + } + + private void validateHasName(ReservationName reservationName, Slot slot) { + if (reservations.stream() + .filter(reservation -> reservation.getSlot().equals(slot)) + .anyMatch(reservation -> reservation.getName().equals(reservationName))) { + throw new RoomEscapeException(ErrorCode.DUPLICATE_RESERVATION); + } + } + + private Status getStatus() { + if (reservations.stream() + .anyMatch(reservation -> reservation.getStatus().equals(Status.APPROVED))) { + return Status.WAITING; + } + return Status.APPROVED; + } +} diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index 436fb29347..f5d89f8e36 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -47,4 +47,18 @@ public ReservationTime getTime() { public Theme getTheme() { return theme; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Slot slot = (Slot) o; + return id == slot.id; + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } } diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index f8d2b99553..2a8281aa81 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -1,6 +1,5 @@ package roomescape.repository; -import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.Optional; @@ -9,10 +8,8 @@ import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservation.ReservationDate; -import roomescape.domain.reservation.ReservationTime; +import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; -import roomescape.domain.theme.Theme; @Repository public class ReservationRepository { @@ -42,25 +39,16 @@ public class ReservationRepository { WHERE id = ? """; private static final String SELECT_BY_ID = SELECT_ALL + "WHERE r.id = ?"; - private static final String SELECT_BY_NAME = SELECT_ALL + "WHERE r.name = ?"; - private static final String EXISTS_BY_DATE_AND_TIME_AND_THEME_ID = """ + private static final String SELECT_BY_SLOT = SELECT_ALL + "WHERE r.slot_id = ?"; + private static final String EXISTS_BY_SLOT_AND_NAME = """ SELECT EXISTS ( SELECT 1 - FROM reservation r - INNER JOIN slot s ON r.slot_id = s.id - WHERE s.date = ? AND s.time_id = ? AND s.theme_id = ? AND r.name = ? - ) - """; - private static final String EXISTS_APPROVED_BY_SLOT = """ - SELECT EXISTS ( - SELECT 1 - FROM reservation r - INNER JOIN slot s ON r.slot_id = s.id - WHERE s.date = ? AND s.time_id = ? AND s.theme_id = ? AND r.status = 'APPROVED' + FROM reservation + WHERE slot_id = ? AND name = ? ) """; private static final String SELECT_FIRST_WAITING_BY_SLOT = SELECT_ALL + """ - WHERE s.date = ? AND rt.id = ? AND t.id = ? AND r.status = 'WAITING' + WHERE r.slot_id = ? AND r.status = 'WAITING' ORDER BY r.created_at, r.id LIMIT 1 """; @@ -96,15 +84,21 @@ public List findAll() { return jdbcTemplate.query(SELECT_ALL, RESERVATION_ROW_MAPPER); } - public List findAllByName(String reservationName) { - return jdbcTemplate.query(SELECT_BY_NAME, RESERVATION_ROW_MAPPER, reservationName); - } - public Optional findById(long reservationId) { List result = jdbcTemplate.query(SELECT_BY_ID, RESERVATION_ROW_MAPPER, reservationId); return result.stream().findFirst(); } + public List findAllBySlot(Slot foundSlot) { + return jdbcTemplate.query(SELECT_BY_SLOT, RESERVATION_ROW_MAPPER, foundSlot.getId()); + } + + public Optional findFirstWaitingBySlot(Slot slot) { + List result = jdbcTemplate.query(SELECT_FIRST_WAITING_BY_SLOT, RESERVATION_ROW_MAPPER, + slot.getId()); + return result.stream().findFirst(); + } + public Reservation save(Reservation reservation) { Map params = Map.of( "name", reservation.getName().getValue(), @@ -125,6 +119,10 @@ public Reservation update(long id, Reservation reservation) { return reservation.withId(id); } + public void updateStatus(Long id, Status status) { + jdbcTemplate.update(UPDATE_STATUS, status.name(), id); + } + public void deleteById(Long id) { jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id); } @@ -139,29 +137,8 @@ public boolean existsByThemeId(long themeId) { jdbcTemplate.queryForObject(EXISTS_BY_THEME_ID, Boolean.class, themeId)); } - public boolean existsByTimeAndThemeAndDateAndName(Long timeId, Long themeId, LocalDate date, String name) { - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(EXISTS_BY_DATE_AND_TIME_AND_THEME_ID, Boolean.class, - date, timeId, themeId, name)); - } - - public boolean existsApprovedByTimeAndThemeAndDate(Long timeId, Long themeId, LocalDate date) { + public boolean existsBySlotAndName(Slot slot, String name) { return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(EXISTS_APPROVED_BY_SLOT, Boolean.class, date, timeId, themeId)); - } - - public Optional findFirstWaitingByTimeAndThemeAndDate(Long timeId, Long themeId, LocalDate date) { - List result = jdbcTemplate.query(SELECT_FIRST_WAITING_BY_SLOT, RESERVATION_ROW_MAPPER, - date, timeId, themeId); - return result.stream().findFirst(); - } - - public void updateStatus(Long id, Status status) { - jdbcTemplate.update(UPDATE_STATUS, status.name(), id); - } - - public List findByTimeAndThemeAndDate(ReservationTime time, Theme theme, ReservationDate date) { - String sql = SELECT_ALL + "WHERE s.date = ? AND t.id = ? AND rt.id = ?"; - return jdbcTemplate.query(sql, RESERVATION_ROW_MAPPER, date.getValue(), theme.getId(), time.getId()); + jdbcTemplate.queryForObject(EXISTS_BY_SLOT_AND_NAME, Boolean.class, slot.getId(), name)); } } diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java index ee495b039b..1b8b6a4e9b 100644 --- a/src/main/java/roomescape/repository/ReservationTimeRepository.java +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -12,11 +12,11 @@ @Repository public class ReservationTimeRepository { - private static final RowMapper RESERVATION_TIME_ROW_MAPPER = (resultSet, rowNum) -> - ReservationTime.of(resultSet.getLong("id"), resultSet.getTime("start_at").toLocalTime()); + private static final RowMapper RESERVATION_TIME_ROW_MAPPER = + (rs, rowNum) -> RepositoryRowMapper.reservationTimeRowMapper(rs); private static final String EXISTS_BY_ID = """ SELECT EXISTS ( - SELECT 1 + SELECT 1 FROM reservation_time WHERE id = ? ) @@ -42,20 +42,19 @@ public ReservationTime save(ReservationTime time) { } public List findAll() { - String sql = "select id, start_at from reservation_time"; - + String sql = "select id AS time_id, start_at from reservation_time"; return jdbcTemplate.query(sql, RESERVATION_TIME_ROW_MAPPER); } public Optional findById(long id) { - String sql = "select id, start_at from reservation_time where id = ?"; + String sql = "select id AS time_id, start_at from reservation_time where id = ?"; List result = jdbcTemplate.query(sql, RESERVATION_TIME_ROW_MAPPER, id); return result.stream().findFirst(); } public List findByDateAndTheme(LocalDate date, long themeId) { String sql = """ - SELECT rt.id, rt.start_at + SELECT rt.id AS time_id, rt.start_at FROM reservation_time AS rt WHERE rt.id NOT IN ( SELECT s.time_id @@ -69,7 +68,6 @@ WHERE rt.id NOT IN ( public void delete(long id) { String sql = "delete from reservation_time where id = ?"; - jdbcTemplate.update(sql, id); } diff --git a/src/main/java/roomescape/repository/SlotRepository.java b/src/main/java/roomescape/repository/SlotRepository.java index 3c4bcbe044..05a4d8f990 100644 --- a/src/main/java/roomescape/repository/SlotRepository.java +++ b/src/main/java/roomescape/repository/SlotRepository.java @@ -8,7 +8,9 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; +import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; +import roomescape.domain.theme.Theme; @Repository public class SlotRepository { @@ -54,9 +56,21 @@ public Optional findById(long id) { return result.stream().findFirst(); } - public Optional findByDateAndTimeAndTheme(LocalDate date, long timeId, long themeId) { + public Optional findByDateAndTimeAndTheme(LocalDate date, ReservationTime time, Theme theme) { String sql = SELECT_ALL + "WHERE s.date = ? AND s.time_id = ? AND s.theme_id = ?"; - List result = jdbcTemplate.query(sql, SLOT_ROW_MAPPER, date, timeId, themeId); + List result = jdbcTemplate.query(sql, SLOT_ROW_MAPPER, date, time.getId(), theme.getId()); return result.stream().findFirst(); } + + public boolean lockSlot(Slot foundSlot) { + String sql = """ + SELECT 1 + FROM slot + WHERE id = ? + FOR UPDATE + """; + + List result = jdbcTemplate.queryForList(sql, Long.class, foundSlot.getId()); + return !result.isEmpty(); + } } diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java index 13ef27314d..ce777264ce 100644 --- a/src/main/java/roomescape/repository/ThemeRepository.java +++ b/src/main/java/roomescape/repository/ThemeRepository.java @@ -9,17 +9,13 @@ import org.springframework.stereotype.Repository; import roomescape.domain.theme.FamousThemeCondition; import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeName; -import roomescape.domain.theme.ThumbnailUrl; @Repository public class ThemeRepository { - public static final RowMapper THEME_ROW_MAPPER = (rs, rowNum) -> - Theme.load(rs.getLong("id"), new ThemeName(rs.getString("name")), rs.getString("description"), - new ThumbnailUrl(rs.getString("thumbnail_url"))); + public static final RowMapper THEME_ROW_MAPPER = (rs, rowNum) -> RepositoryRowMapper.themeRowMapper(rs); private static final String EXISTS_BY_ID = """ SELECT EXISTS ( - SELECT 1 + SELECT 1 FROM theme WHERE id = ? ) @@ -46,13 +42,13 @@ public Theme save(Theme theme) { } public List findAll() { - String sql = "SELECT id, name, description, thumbnail_url FROM THEME"; + String sql = "SELECT id AS theme_id, name AS theme_name, description, thumbnail_url FROM THEME"; return jdbcTemplate.query(sql, THEME_ROW_MAPPER); } public List findFamous(FamousThemeCondition condition) { String sql = """ - SELECT t.id, t.name, t.description, t.thumbnail_url + SELECT t.id AS theme_id, t.name AS theme_name, t.description, t.thumbnail_url FROM THEME AS t INNER JOIN ( SELECT s.theme_id, count(s.theme_id) AS cnt @@ -81,7 +77,7 @@ public boolean existsById(long themeId) { } public Optional findById(long themeId) { - String sql = "SELECT id, name, description, thumbnail_url FROM THEME WHERE id = ?"; + String sql = "SELECT id AS theme_id, name AS theme_name, description, thumbnail_url FROM THEME WHERE id = ?"; List result = jdbcTemplate.query(sql, THEME_ROW_MAPPER, themeId); return result.stream().findFirst(); } diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 623747376d..a8b2a60f73 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -2,10 +2,8 @@ import common.exception.ErrorCode; import common.exception.RoomEscapeException; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.controller.dto.request.ReservationCreateRequest; @@ -13,74 +11,40 @@ import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.RankedReservations; import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationName; -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.repository.ReservationRepository; -import roomescape.repository.ReservationTimeRepository; -import roomescape.repository.SlotRepository; -import roomescape.repository.ThemeRepository; @Service @Transactional(readOnly = true) public class ReservationService { + private final SlotService slotService; private final ReservationRepository reservationRepository; - private final ReservationTimeRepository reservationTimeRepository; - private final ThemeRepository themeRepository; - private final SlotRepository slotRepository; - public ReservationService(ReservationRepository reservationRepository, - ReservationTimeRepository reservationTimeRepository, ThemeRepository themeRepository, - SlotRepository slotRepository) { + public ReservationService(SlotService slotService, ReservationRepository reservationRepository) { + this.slotService = slotService; this.reservationRepository = reservationRepository; - this.reservationTimeRepository = reservationTimeRepository; - this.themeRepository = themeRepository; - this.slotRepository = slotRepository; } @Transactional public RankedReservation reserve(ReservationCreateRequest request, LocalDateTime now) { - ReservationTime reservationTime = findReservationTimeByTimeId(request.getTimeId()); - Theme theme = findThemeByThemeId(request.getThemeId()); - - validateIsDuplicateNameReservation(request.getTimeId(), request.getThemeId(), request.getDate(), - request.getName()); - Status status = determineStatus(request.getTimeId(), request.getThemeId(), request.getDate()); - - Slot slot = null; - try { - slot = slotRepository.findByDateAndTimeAndTheme(request.getDate(), reservationTime.getId(), - theme.getId()).orElseGet(() -> slotRepository.save( - Slot.create(new ReservationDate(request.getDate()), reservationTime, theme))); - - } catch (DataIntegrityViolationException e) { - slot = slotRepository.findByDateAndTimeAndTheme(request.getDate(), reservationTime.getId(), theme.getId()) - .get(); - status = Status.WAITING; - } + Slot foundSlot = slotService.findOrCreate(request.getDate(), request.getTimeId(), request.getThemeId()); + slotService.lockSlot(foundSlot); - Reservation reservation = Reservation.create(new ReservationName(request.getName()), slot, status, now); - Reservation saved = reservationRepository.save(reservation); + Reservations reservations = new Reservations(reservationRepository.findAllBySlot(foundSlot)); + Reservation reservation = reservations.reserve(new ReservationName(request.getName()), foundSlot, now); - return getRankedReservation(saved); + return getRankedReservation(reservationRepository.save(reservation)); } private RankedReservation getRankedReservation(Reservation target) { - List reservations = reservationRepository.findByTimeAndThemeAndDate( - target.getTime(), target.getTheme(), target.getDate()); - return RankedReservation.decideRankFrom(target, reservations); + return RankedReservation.decideRankFrom(target, reservationRepository.findAllBySlot(target.getSlot())); } public RankedReservation find(long reservationId) { - Reservation reservation = findReservationById(reservationId); - - List sameSlots = reservationRepository.findByTimeAndThemeAndDate( - reservation.getTime(), reservation.getTheme(), reservation.getDate()); - - return RankedReservation.decideRankFrom(reservation, sameSlots); + return getRankedReservation(findReservationById(reservationId)); } public List findList(String name) { @@ -92,35 +56,31 @@ public List findList(String name) { return rankedReservations.resultsOf(name); } - @Transactional public RankedReservation update(ReservationUpdateRequest request, long id, LocalDateTime now) { Reservation originReservation = findReservationById(id); originReservation.ensureNotPast(now); - ReservationDate reservationDateToUpdate = new ReservationDate(request.getDate()); - ReservationTime reservationTimeToUpdate = findReservationTimeByTimeId(request.getTimeId()); + Slot updateSlot = slotService.findOrCreate(request.getDate(), request.getTimeId(), request.getThemeId()); + slotService.lockSlot(updateSlot); - validateIsDuplicateNameReservation(request.getTimeId(), request.getThemeId(), request.getDate(), - request.getName()); + validateIsDuplicateReservation(updateSlot, request.getName()); - Slot slotToUpdate = findOrCreateSlot(reservationDateToUpdate, reservationTimeToUpdate, - originReservation.getTheme()); + Reservations reservations = new Reservations(reservationRepository.findAllBySlot(updateSlot)); + Reservation reserved = reservations.reserve(new ReservationName(request.getName()), updateSlot, now); - Status status = determineStatus(request.getTimeId(), request.getThemeId(), request.getDate()); - Reservation target = Reservation.create(originReservation.getName(), slotToUpdate, status, now); + Reservation updated = reservationRepository.update(id, reserved); - Reservation updated = reservationRepository.update(id, target); - - reservationRepository.findFirstWaitingByTimeAndThemeAndDate( - originReservation.getTime().getId(), - originReservation.getTheme().getId(), - originReservation.getDate().getValue() - ).ifPresent(waiting -> reservationRepository.updateStatus(waiting.getId(), Status.APPROVED)); + findFirstWaitingAndUpdateStatus(originReservation); return getRankedReservation(updated); } + private void findFirstWaitingAndUpdateStatus(Reservation reservation) { + reservationRepository.findFirstWaitingBySlot(reservation.getSlot()) + .ifPresent(waiting -> reservationRepository.updateStatus(waiting.getId(), Status.APPROVED)); + } + @Transactional public void cancel(long reservationId, LocalDateTime now) { Reservation reservation = findReservationById(reservationId); @@ -130,38 +90,12 @@ public void cancel(long reservationId, LocalDateTime now) { reservationRepository.deleteById(reservationId); if (cancelledStatus == Status.APPROVED) { - reservationRepository.findFirstWaitingByTimeAndThemeAndDate( - reservation.getTime().getId(), - reservation.getTheme().getId(), - reservation.getDate().getValue() - ).ifPresent(waiting -> reservationRepository.updateStatus(waiting.getId(), Status.APPROVED)); + findFirstWaitingAndUpdateStatus(reservation); } } - private Slot findOrCreateSlot(ReservationDate date, ReservationTime time, Theme theme) { - return slotRepository.findByDateAndTimeAndTheme(date.getValue(), time.getId(), theme.getId()) - .orElseGet(() -> slotRepository.save(Slot.create(date, time, theme))); - } - - private Status determineStatus(long timeId, long themeId, LocalDate date) { - if (reservationRepository.existsApprovedByTimeAndThemeAndDate(timeId, themeId, date)) { - return Status.WAITING; - } - return Status.APPROVED; - } - - private ReservationTime findReservationTimeByTimeId(long reservationTimeId) { - return reservationTimeRepository.findById(reservationTimeId) - .orElseThrow(() -> new RoomEscapeException(ErrorCode.RESERVATION_TIME_NOT_FOUND)); - } - - private Theme findThemeByThemeId(long themeId) { - return themeRepository.findById(themeId).orElseThrow( - () -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); - } - - private void validateIsDuplicateNameReservation(long timeId, long themeId, LocalDate date, String name) { - if (reservationRepository.existsByTimeAndThemeAndDateAndName(timeId, themeId, date, name)) { + private void validateIsDuplicateReservation(Slot slot, String name) { + if (reservationRepository.existsBySlotAndName(slot, name)) { throw new RoomEscapeException(ErrorCode.DUPLICATE_RESERVATION); } } diff --git a/src/main/java/roomescape/service/SlotService.java b/src/main/java/roomescape/service/SlotService.java new file mode 100644 index 0000000000..ce66562aeb --- /dev/null +++ b/src/main/java/roomescape/service/SlotService.java @@ -0,0 +1,73 @@ +package roomescape.service; + +import common.exception.ErrorCode; +import common.exception.RoomEscapeException; +import java.time.LocalDate; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import roomescape.domain.reservation.ReservationDate; +import roomescape.domain.reservation.ReservationTime; +import roomescape.domain.reservation.Slot; +import roomescape.domain.theme.Theme; +import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.SlotRepository; +import roomescape.repository.ThemeRepository; + +@Service +@Transactional(readOnly = true) +public class SlotService { + private final SlotRepository slotRepository; + private final ReservationTimeRepository reservationTimeRepository; + private final ThemeRepository themeRepository; + + public SlotService(SlotRepository slotRepository, ReservationTimeRepository reservationTimeRepository, + ThemeRepository themeRepository) { + this.slotRepository = slotRepository; + this.reservationTimeRepository = reservationTimeRepository; + this.themeRepository = themeRepository; + } + + @Transactional + public Slot create(LocalDate date, long timeId, long themeId) { + ReservationTime time = reservationTimeRepository.findById(timeId) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); + Theme theme = themeRepository.findById(themeId) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); + + return slotRepository.findByDateAndTimeAndTheme(date, time, theme) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND)); + + } + + @Transactional + public Slot findOrCreate(LocalDate date, long timeId, long themeId) { + ReservationTime time = reservationTimeRepository.findById(timeId) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.RESERVATION_TIME_NOT_FOUND)); + Theme theme = themeRepository.findById(themeId) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); + + return slotRepository.findByDateAndTimeAndTheme(date, time, theme) + .orElseGet(() -> saveOrReread(date, time, theme)); + } + + private Slot saveOrReread(LocalDate date, ReservationTime time, Theme theme) { + try { + return slotRepository.save(Slot.create(new ReservationDate(date), time, theme)); + } catch (DataIntegrityViolationException e) { + return slotRepository.findByDateAndTimeAndTheme(date, time, theme) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND)); + } + } + + public Slot findById(long slotId) { + return slotRepository.findById(slotId) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND)); + } + + public void lockSlot(Slot foundSlot) { + if (!slotRepository.lockSlot(foundSlot)) { + throw new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND); + } + } +} diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 85ecef6e39..b043cf7ce0 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -17,7 +17,7 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class MissionStepTest { +class MissionStepTest { @Autowired private JdbcTemplate jdbcTemplate; diff --git a/src/test/java/roomescape/domain/reservation/ReservationsTest.java b/src/test/java/roomescape/domain/reservation/ReservationsTest.java new file mode 100644 index 0000000000..b10636a719 --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/ReservationsTest.java @@ -0,0 +1,65 @@ +package roomescape.domain.reservation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import common.exception.RoomEscapeException; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import roomescape.RoomEscapeFixture; + +class ReservationsTest { + private static final Slot SLOT = RoomEscapeFixture.slot().build(); + private static final ReservationName NAME = new ReservationName("zeze"); + private static final LocalDateTime NOW = RoomEscapeFixture.PAST_DATE_TIME; + + @Test + void 예약이_없으면_APPROVED로_생성된다() { + Reservations reservations = new Reservations(List.of()); + + Reservation result = reservations.reserve(NAME, SLOT, NOW); + + assertThat(result.getStatus()).isEqualTo(Status.APPROVED); + } + + @Test + void APPROVED_예약이_있으면_WAITING으로_생성된다() { + Reservation existing = RoomEscapeFixture.reservation().status(Status.APPROVED).build(); + Reservations reservations = new Reservations(List.of(existing)); + + Reservation result = reservations.reserve(new ReservationName("달수"), SLOT, NOW); + + assertThat(result.getStatus()).isEqualTo(Status.WAITING); + } + + @Test + void 같은_슬롯에_같은_이름이_있으면_예외가_발생한다() { + Reservation existing = RoomEscapeFixture.reservation().build(); + Reservations reservations = new Reservations(List.of(existing)); + + assertThatThrownBy(() -> reservations.reserve(NAME, SLOT, NOW)) + .isInstanceOf(RoomEscapeException.class); + } + + @Test + void 같은_슬롯에_다른_이름이면_예약이_가능하다() { + Reservation existing = RoomEscapeFixture.reservation().name("달수").build(); + Reservations reservations = new Reservations(List.of(existing)); + + Reservation result = reservations.reserve(NAME, SLOT, NOW); + + assertThat(result.getName()).isEqualTo(NAME); + } + + @Test + void 다른_슬롯에_같은_이름이면_예약이_가능하다() { + Slot otherSlot = RoomEscapeFixture.slot().id(2L).build(); + Reservation existing = RoomEscapeFixture.reservation().slot(otherSlot).build(); + Reservations reservations = new Reservations(List.of(existing)); + + Reservation result = reservations.reserve(NAME, SLOT, NOW); + + assertThat(result.getName()).isEqualTo(NAME); + } +} diff --git a/src/test/java/roomescape/domain/theme/ThumbnailUrlTest.java b/src/test/java/roomescape/domain/theme/ThumbnailUrlTest.java index 5c290beee6..3a440b5c0a 100644 --- a/src/test/java/roomescape/domain/theme/ThumbnailUrlTest.java +++ b/src/test/java/roomescape/domain/theme/ThumbnailUrlTest.java @@ -4,7 +4,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -public class ThumbnailUrlTest { +class ThumbnailUrlTest { @Test void 유효하지_않은_형식으로_생성시_예외가_발생한다() { Assertions.assertThatThrownBy(() -> new ThumbnailUrl("zeze.com")).isInstanceOf(RoomEscapeException.class); diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java index 1b679a4043..d5ac11594f 100644 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ b/src/test/java/roomescape/repository/ReservationRepositoryTest.java @@ -3,12 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; -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 org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -34,11 +31,6 @@ SlotRepository.class }) class ReservationRepositoryTest { - private static final Clock FIXED_CLOCK = Clock.fixed( - Instant.parse("2026-05-10T03:00:00Z"), - ZoneId.of("Asia/Seoul") - ); - private static final LocalDate TODAY = LocalDate.of(2026, 5, 10); private static final LocalDate FUTURE = LocalDate.of(2099, 1, 1); @@ -54,17 +46,21 @@ class ReservationRepositoryTest { @Autowired private SlotRepository slotRepository; - private ReservationTime giveTime(int hour) { + private ReservationTime saveTimeAndGet(int hour) { return timeRepository.save(ReservationTime.of(LocalTime.of(hour, 0))); } - private Theme giveTheme(String name) { + private Theme saveThemeAndGet(String name) { return themeRepository.save( - Theme.create(new ThemeName(name), name + "테마에 관한 설명 입니다.", new ThumbnailUrl("https://test-theme.com"))); + Theme.create(new ThemeName(name), name + " 설명", new ThumbnailUrl("https://test-theme.com"))); + } + + private Theme saveThemeAndGet() { + return saveThemeAndGet("테마"); } - private Slot giveSlot(LocalDate date, ReservationTime time, Theme theme) { - return slotRepository.findByDateAndTimeAndTheme(date, time.getId(), theme.getId()) + private Slot getSlotOrCreate(LocalDate date, ReservationTime time, Theme theme) { + return slotRepository.findByDateAndTimeAndTheme(date, time, theme) .orElseGet(() -> slotRepository.save(Slot.create(new ReservationDate(date), time, theme))); } @@ -72,10 +68,9 @@ private Reservation reservation(String name, LocalDate date, ReservationTime tim return reservation(name, date, time, theme, Status.APPROVED); } - private Reservation reservation(String name, LocalDate date, ReservationTime time, Theme theme, - Status status) { - Slot slot = giveSlot(date, time, theme); - return Reservation.create(new ReservationName(name), slot, status, LocalDateTime.now(FIXED_CLOCK)); + private Reservation reservation(String name, LocalDate date, ReservationTime time, Theme theme, Status status) { + Slot slot = getSlotOrCreate(date, time, theme); + return Reservation.load(0L, new ReservationName(name), slot, status, LocalDateTime.now()); } @Nested @@ -84,22 +79,22 @@ class Save { @Test void 예약을_저장하면_ID가_부여된_예약이_반환된다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); - Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme)); + Reservation saved = reservationRepository.save(reservation("제제", FUTURE, time, theme)); assertSoftly(soft -> { soft.assertThat(saved.getId()).isPositive(); - soft.assertThat(saved.getName().getValue()).isEqualTo("달수"); + soft.assertThat(saved.getName().getValue()).isEqualTo("제제"); soft.assertThat(saved.getDate().getValue()).isEqualTo(FUTURE); }); } @Test void 여러_예약을_저장하면_각기_다른_ID가_부여된다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); Reservation first = reservationRepository.save(reservation("달수", FUTURE, time, theme)); Reservation second = reservationRepository.save(reservation("민구", FUTURE.plusDays(1), time, theme)); @@ -119,8 +114,8 @@ class FindAll { @Test void 저장된_예약을_모두_반환한다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); reservationRepository.save(reservation("달수", FUTURE, time, theme)); reservationRepository.save(reservation("민구", FUTURE.plusDays(1), time, theme)); @@ -130,24 +125,28 @@ class FindAll { } @Nested - @DisplayName("findAllByName") + @DisplayName("findAll (name filter)") class FindAllByName { @Test void 이름으로_조회하면_해당_이름의_예약만_반환된다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); reservationRepository.save(reservation("달수", FUTURE, time, theme)); reservationRepository.save(reservation("달수", FUTURE.plusDays(1), time, theme)); reservationRepository.save(reservation("민구", FUTURE.plusDays(2), time, theme)); - assertThat(reservationRepository.findAllByName("달수")).hasSize(2); + assertThat(reservationRepository.findAll().stream() + .filter(r -> r.getName().getValue().equals("달수")) + .toList()).hasSize(2); } @Test void 존재하지_않는_이름으로_조회하면_빈_목록을_반환한다() { - assertThat(reservationRepository.findAllByName("없는이름")).isEmpty(); + assertThat(reservationRepository.findAll().stream() + .filter(r -> r.getName().getValue().equals("없는이름")) + .toList()).isEmpty(); } } @@ -157,8 +156,8 @@ class FindById { @Test void ID로_조회하면_해당_예약이_반환된다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme)); @@ -172,8 +171,8 @@ class FindById { @Test void 조회한_예약의_필드가_저장된_값과_일치한다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme)); Reservation found = reservationRepository.findById(saved.getId()).orElseThrow(); @@ -193,9 +192,9 @@ class Update { @Test void 예약을_수정하면_변경된_내용이_반영된다() { - Theme theme = giveTheme("테마1"); - ReservationTime time1 = giveTime(10); - ReservationTime time2 = giveTime(14); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time1 = saveTimeAndGet(10); + ReservationTime time2 = saveTimeAndGet(14); Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time1, theme)); Reservation target = reservation("민구", FUTURE.plusDays(1), time2, theme); @@ -217,8 +216,8 @@ class DeleteById { @Test void 예약을_삭제하면_조회할_수_없다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme)); reservationRepository.deleteById(saved.getId()); @@ -228,8 +227,8 @@ class DeleteById { @Test void 예약을_삭제하면_전체_목록에서도_제외된다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); Reservation r1 = reservationRepository.save(reservation("달수", FUTURE, time, theme)); reservationRepository.save(reservation("민구", FUTURE.plusDays(1), time, theme)); @@ -241,31 +240,31 @@ class DeleteById { } @Nested - @DisplayName("findByTimeAndThemeAndDate") - class FindByTimeAndThemeAndDate { + @DisplayName("findAllBySlot") + class FindAllBySlot { @Test - void 같은_시간_테마_날짜의_예약을_모두_반환한다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + void 같은_슬롯의_예약을_모두_반환한다() { + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); reservationRepository.save(reservation("달수", FUTURE, time, theme)); reservationRepository.save(reservation("민구", FUTURE, time, theme)); - assertThat(reservationRepository.findByTimeAndThemeAndDate(time, theme, - new ReservationDate(FUTURE))).hasSize(2); + Slot slot = getSlotOrCreate(FUTURE, time, theme); + assertThat(reservationRepository.findAllBySlot(slot)).hasSize(2); } @Test void 다른_날짜의_예약은_포함되지_않는다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); reservationRepository.save(reservation("달수", FUTURE, time, theme)); reservationRepository.save(reservation("민구", FUTURE.plusDays(1), time, theme)); - assertThat(reservationRepository.findByTimeAndThemeAndDate(time, theme, - new ReservationDate(FUTURE))).hasSize(1); + Slot slot = getSlotOrCreate(FUTURE, time, theme); + assertThat(reservationRepository.findAllBySlot(slot)).hasSize(1); } } @@ -275,8 +274,8 @@ class ExistsByFk { @Test void 해당_시간으로_예약이_있으면_true를_반환한다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); reservationRepository.save(reservation("달수", FUTURE, time, theme)); @@ -285,15 +284,15 @@ class ExistsByFk { @Test void 해당_시간으로_예약이_없으면_false를_반환한다() { - ReservationTime time = giveTime(10); + ReservationTime time = saveTimeAndGet(10); assertThat(reservationRepository.existsByTimeId(time.getId())).isFalse(); } @Test void 해당_테마로_예약이_있으면_true를_반환한다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(10); + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(10); reservationRepository.save(reservation("달수", FUTURE, time, theme)); @@ -302,112 +301,106 @@ class ExistsByFk { @Test void 해당_테마로_예약이_없으면_false를_반환한다() { - Theme theme = giveTheme("테마1"); + Theme theme = saveThemeAndGet("테마1"); assertThat(reservationRepository.existsByThemeId(theme.getId())).isFalse(); } } @Nested - @DisplayName("existsByTimeAndThemeAndDateAndName") + @DisplayName("existsBySlotAndName") class Exists { @Test - void 예약을_할_때_같은_슬롯이면_true() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(14); + void 같은_슬롯과_이름이면_true() { + Theme theme = saveThemeAndGet("테마1"); + ReservationTime time = saveTimeAndGet(14); String name = "달수"; reservationRepository.save(reservation(name, TODAY, time, theme)); - assertThat(reservationRepository.existsByTimeAndThemeAndDateAndName(time.getId(), theme.getId(), - TODAY, name)).isTrue(); + Slot slot = getSlotOrCreate(TODAY, time, theme); + assertThat(reservationRepository.existsBySlotAndName(slot, name)).isTrue(); } @Test - void 예약을_할_때_슬롯의_이름_날짜_시간_테마가_하나라도_다르면_false() { - Theme theme1 = giveTheme("테마1"); - Theme theme2 = giveTheme("테마2"); - ReservationTime time1 = giveTime(14); - ReservationTime time2 = giveTime(15); + void 슬롯이나_이름이_다르면_false() { + Theme theme1 = saveThemeAndGet("테마1"); + Theme theme2 = saveThemeAndGet("테마2"); + ReservationTime time1 = saveTimeAndGet(14); String name = "달수"; reservationRepository.save(reservation(name, TODAY, time1, theme1)); + Slot slot1 = getSlotOrCreate(TODAY, time1, theme1); + assertSoftly(soft -> { - soft.assertThat( - reservationRepository.existsByTimeAndThemeAndDateAndName(time1.getId(), theme1.getId(), TODAY, - "other")).isFalse(); - soft.assertThat( - reservationRepository.existsByTimeAndThemeAndDateAndName(time1.getId(), theme2.getId(), TODAY, - name)) - .isFalse(); - soft.assertThat( - reservationRepository.existsByTimeAndThemeAndDateAndName(time2.getId(), theme1.getId(), TODAY, - name)) - .isFalse(); - soft.assertThat(reservationRepository.existsByTimeAndThemeAndDateAndName(time1.getId(), theme1.getId(), - TODAY.plusDays(1), name)).isFalse(); + soft.assertThat(reservationRepository.existsBySlotAndName(slot1, "other")).isFalse(); + Slot slot2 = getSlotOrCreate(TODAY, time1, theme2); + soft.assertThat(reservationRepository.existsBySlotAndName(slot2, name)).isFalse(); }); } } @Nested - @DisplayName("existsApprovedByTimeAndThemeAndDate") + @DisplayName("existsApproved (slot 기준)") class ExistsApproved { @Test void APPROVED_예약이_있으면_true() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(14); + Theme theme = saveThemeAndGet(); + ReservationTime time = saveTimeAndGet(14); reservationRepository.save(reservation("달수", TODAY, time, theme, Status.APPROVED)); - assertThat(reservationRepository.existsApprovedByTimeAndThemeAndDate(time.getId(), theme.getId(), - TODAY)).isTrue(); + Slot slot = getSlotOrCreate(TODAY, time, theme); + assertThat(reservationRepository.findAllBySlot(slot).stream() + .anyMatch(r -> r.getStatus() == Status.APPROVED)).isTrue(); } @Test void WAITING_예약만_있으면_false() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(14); + Theme theme = saveThemeAndGet(); + ReservationTime time = saveTimeAndGet(14); reservationRepository.save(reservation("달수", TODAY, time, theme, Status.WAITING)); - assertThat(reservationRepository.existsApprovedByTimeAndThemeAndDate(time.getId(), theme.getId(), - TODAY)).isFalse(); + Slot slot = getSlotOrCreate(TODAY, time, theme); + assertThat(reservationRepository.findAllBySlot(slot).stream() + .anyMatch(r -> r.getStatus() == Status.APPROVED)).isFalse(); } } @Nested - @DisplayName("findFirstWaitingByTimeAndThemeAndDate") + @DisplayName("findFirstWaitingBySlot") class FindFirstWaiting { @Test void WAITING_예약이_있으면_가장_먼저_생성된_예약을_반환한다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(14); + Theme theme = saveThemeAndGet(); + ReservationTime time = saveTimeAndGet(14); reservationRepository.save(reservation("달수", TODAY, time, theme, Status.APPROVED)); Reservation first = reservationRepository.save(reservation("민구", TODAY, time, theme, Status.WAITING)); reservationRepository.save(reservation("철수", TODAY, time, theme, Status.WAITING)); - assertThat(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(time.getId(), theme.getId(), TODAY)) + Slot slot = getSlotOrCreate(TODAY, time, theme); + assertThat(reservationRepository.findFirstWaitingBySlot(slot)) .isPresent() .get() - .extracting(r -> r.getId()) + .extracting(Reservation::getId) .isEqualTo(first.getId()); } @Test void WAITING_예약이_없으면_빈_Optional을_반환한다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(14); + Theme theme = saveThemeAndGet(); + ReservationTime time = saveTimeAndGet(14); reservationRepository.save(reservation("달수", TODAY, time, theme, Status.APPROVED)); - assertThat(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(time.getId(), theme.getId(), - TODAY)).isEmpty(); + Slot slot = getSlotOrCreate(TODAY, time, theme); + assertThat(reservationRepository.findFirstWaitingBySlot(slot)).isEmpty(); } } @@ -417,16 +410,15 @@ class UpdateStatus { @Test void WAITING_예약을_APPROVED로_변경할_수_있다() { - Theme theme = giveTheme("테마1"); - ReservationTime time = giveTime(14); - - Reservation saved = reservationRepository.save(reservation("달수", TODAY, time, theme, Status.WAITING)); + Theme theme = saveThemeAndGet(); + ReservationTime time = saveTimeAndGet(14); + Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme, Status.WAITING)); reservationRepository.updateStatus(saved.getId(), Status.APPROVED); assertThat(reservationRepository.findById(saved.getId())) .isPresent() .get() - .extracting(r -> r.getStatus()) + .extracting(Reservation::getStatus) .isEqualTo(Status.APPROVED); } } diff --git a/src/test/java/roomescape/repository/SlotRepositoryTest.java b/src/test/java/roomescape/repository/SlotRepositoryTest.java index 9e7526aaa7..b31076bc43 100644 --- a/src/test/java/roomescape/repository/SlotRepositoryTest.java +++ b/src/test/java/roomescape/repository/SlotRepositoryTest.java @@ -92,7 +92,7 @@ class FindByDateAndTimeAndTheme { Slot saved = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); - assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE, time.getId(), theme.getId())) + assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE, time, theme)) .isPresent() .get() .extracting(Slot::getId) @@ -102,34 +102,16 @@ class FindByDateAndTimeAndTheme { @Test void 조건이_하나라도_다르면_빈_Optional을_반환한다() { ReservationTime time = giveTime(10); + ReservationTime otherTime = giveTime(11); Theme theme = giveTheme("테마1"); + Theme otherTheme = giveTheme("테마2"); slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); assertSoftly(soft -> { - soft.assertThat(slotRepository.findByDateAndTimeAndTheme( - FUTURE.plusDays(1), time.getId(), theme.getId())).isEmpty(); - soft.assertThat(slotRepository.findByDateAndTimeAndTheme( - FUTURE, time.getId() + 1, theme.getId())).isEmpty(); - soft.assertThat(slotRepository.findByDateAndTimeAndTheme( - FUTURE, time.getId(), theme.getId() + 1)).isEmpty(); + soft.assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE.plusDays(1), time, theme)).isEmpty(); + soft.assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE, otherTime, theme)).isEmpty(); + soft.assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE, time, otherTheme)).isEmpty(); }); } } - - @Nested - @DisplayName("UNIQUE 제약") - class UniqueConstraint { - - @Test - void 동일한_날짜_시간_테마로_두번_저장하면_두번째는_기존_슬롯이_조회된다() { - ReservationTime time = giveTime(10); - Theme theme = giveTheme("테마1"); - - Slot first = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); - Slot found = slotRepository.findByDateAndTimeAndTheme(FUTURE, time.getId(), theme.getId()) - .orElseThrow(); - - assertThat(first.getId()).isEqualTo(found.getId()); - } - } } diff --git a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java index a84930fe8e..05b52e30c5 100644 --- a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java +++ b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java @@ -43,7 +43,7 @@ void init() { @Test void 동시에_10명이_첫_예약_요청시_1명만_승인상태가_된다() throws Exception { - // 한 슬롯에 Approve된 예약은 반드시 1건 미만이어야 한다. + // 한 슬롯에 Approve된 예약은 반드시 1건 이하여야 한다. int threads = 10; var ready = new CountDownLatch(threads); var start = new CountDownLatch(1); diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index 42ce9a6217..4ab8d439e1 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -10,7 +10,6 @@ import common.exception.RoomEscapeException; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.List; import java.util.Optional; import org.assertj.core.api.Assertions; @@ -26,25 +25,16 @@ import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservation.ReservationDate; -import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; -import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeName; -import roomescape.domain.theme.ThumbnailUrl; import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeRepository; -import roomescape.repository.SlotRepository; -import roomescape.repository.ThemeRepository; @ExtendWith(MockitoExtension.class) class ReservationServiceTest { - private static final String URL = "https://zeze.com/thumb.jpg"; private static final String NAME = "제제"; - private static final LocalDateTime TODAY = LocalDateTime.of(2026, 5, 10, 10, 0, 0); - private static final Slot DUMMY_SLOT = RoomEscapeFixture.slot().build(); - private static final Reservation DUMMY = RoomEscapeFixture.reservation().name(NAME).createdAt(TODAY).build(); + private static final LocalDateTime NOW = LocalDateTime.of(2026, 5, 10, 10, 0, 0); + private static final Slot SLOT = RoomEscapeFixture.slot().build(); + private static final Reservation DUMMY = RoomEscapeFixture.reservation().name(NAME).createdAt(NOW).build(); private static final long NOT_EXISTS_ID = Long.MAX_VALUE; private static final long EXISTS_ID = 1L; @@ -52,13 +42,7 @@ class ReservationServiceTest { private ReservationRepository reservationRepository; @Mock - private ReservationTimeRepository reservationTimeRepository; - - @Mock - private ThemeRepository themeRepository; - - @Mock - private SlotRepository slotRepository; + private SlotService slotService; @InjectMocks private ReservationService reservationService; @@ -68,39 +52,48 @@ class ReservationServiceTest { class Reserve { @Test - void 존재하지_않는_시간으로_예약시_예외가_발생한다() { - given(reservationTimeRepository.findById(1L)).willReturn(Optional.empty()); + void 슬롯_조회_실패시_예외가_발생한다() { + given(slotService.findOrCreate(any(), anyLong(), anyLong())) + .willThrow(new RoomEscapeException(ErrorCode.RESERVATION_TIME_NOT_FOUND)); ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequest(); - Assertions.assertThatThrownBy(() -> reservationService.reserve(request, LocalDateTime.MAX)) + Assertions.assertThatThrownBy(() -> reservationService.reserve(request, NOW)) .isInstanceOf(RoomEscapeException.class); } + @Test + void 슬롯에_예약이_없으면_APPROVED로_예약된다() { + given(slotService.findOrCreate(any(), anyLong(), anyLong())).willReturn(SLOT); + given(reservationRepository.findAllBySlot(any())) + .willReturn(List.of()) + .willReturn(List.of(DUMMY)); + given(reservationRepository.save(any())).willReturn(DUMMY); + + ReservationCreateRequest request = new ReservationCreateRequest(NAME, + LocalDate.of(2099, 11, 11), 1L, 1L); + + RankedReservation result = reservationService.reserve(request, NOW); + + Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.APPROVED); + } + @Test void APPROVED가_이미_있으면_WAITING으로_예약된다() { - ReservationTime reservationTime = ReservationTime.of(LocalTime.parse("11:00")); - Theme theme = Theme.load(1L, new ThemeName("테마1"), "설명", new ThumbnailUrl(URL)); - Slot waitingSlot = RoomEscapeFixture.slot().id(2L) - .date(new ReservationDate(LocalDate.parse("2026-04-05"))).time(reservationTime).theme(theme) - .build(); - Reservation waitingSaved = RoomEscapeFixture.reservation().id(2L).name("zeze").slot(waitingSlot) - .status(Status.WAITING).createdAt(TODAY).build(); - - ReservationCreateRequest request = new ReservationCreateRequest("zeze", - LocalDate.parse("2026-04-05"), 1L, 1L); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); - given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); - given(reservationRepository.existsApprovedByTimeAndThemeAndDate(anyLong(), anyLong(), any())) - .willReturn(true); - given(slotRepository.findByDateAndTimeAndTheme(any(), anyLong(), anyLong())) - .willReturn(Optional.of(waitingSlot)); + Reservation existing = RoomEscapeFixture.reservation().name("기존").createdAt(NOW).build(); + Reservation waitingSaved = RoomEscapeFixture.reservation().id(2L).name("달수") + .status(Status.WAITING).createdAt(NOW).build(); + + given(slotService.findOrCreate(any(), anyLong(), anyLong())).willReturn(SLOT); + given(reservationRepository.findAllBySlot(any())) + .willReturn(List.of(existing)) + .willReturn(List.of(existing, waitingSaved)); given(reservationRepository.save(any())).willReturn(waitingSaved); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())) - .willReturn(List.of(DUMMY, waitingSaved)); - RankedReservation result = reservationService.reserve(request, - LocalDateTime.of(2026, 4, 5, 10, 59, 59)); + ReservationCreateRequest request = new ReservationCreateRequest("달수", + LocalDate.of(2099, 11, 11), 1L, 1L); + + RankedReservation result = reservationService.reserve(request, NOW); Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.WAITING); } @@ -113,7 +106,7 @@ class Find { @Test void 존재하는_ID면_결과를_반환한다() { given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); - given(reservationRepository.findByTimeAndThemeAndDate(any(), any(), any())).willReturn(List.of(DUMMY)); + given(reservationRepository.findAllBySlot(any())).willReturn(List.of(DUMMY)); RankedReservation result = reservationService.find(EXISTS_ID); @@ -161,49 +154,26 @@ class Update { @Test void ID가_없으면_예외가_발생한다() { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", - LocalDate.parse("2099-04-06"), 1L, 1L); - given(reservationRepository.findById(999L)).willReturn(Optional.empty()); - - Assertions.assertThatThrownBy(() -> reservationService.update(request, 999L, LocalDateTime.MIN)) - .isInstanceOf(RoomEscapeException.class); - } + given(reservationRepository.findById(NOT_EXISTS_ID)).willReturn(Optional.empty()); - @Test - void 과거_날짜의_예약이면_예외가_발생한다() { ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", - LocalDate.parse("2000-04-06"), 1L, 1L); - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); - - Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MAX)) - .isInstanceOf(RoomEscapeException.class); - } + LocalDate.of(2099, 4, 6), 1L, 1L); - @Test - void 시간을_찾을_수_없으면_예외가_발생한다() { - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", - LocalDate.parse("2099-04-06"), 1L, 1L); - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.empty()); - - Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MIN)) + Assertions.assertThatThrownBy(() -> reservationService.update(request, NOT_EXISTS_ID, NOW)) .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorCode.RESERVATION_TIME_NOT_FOUND.getMessage()); + .hasMessage(ErrorCode.RESERVATION_NOT_FOUND.getMessage()); } @Test - void 이름_중복이면_예외가_발생한다() { - ReservationTime reservationTime = ReservationTime.of(1L, LocalTime.parse("11:00")); + void 과거_날짜의_예약이면_예외가_발생한다() { + given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); + ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", - LocalDate.parse("2099-04-06"), 1L, 1L); - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); - given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(reservationTime)); - given(reservationRepository.existsByTimeAndThemeAndDateAndName(request.getTimeId(), - request.getThemeId(), request.getDate(), request.getName())).willReturn(true); + LocalDate.of(2000, 4, 6), 1L, 1L); - Assertions.assertThatThrownBy(() -> reservationService.update(request, 1L, LocalDateTime.MIN)) + Assertions.assertThatThrownBy(() -> reservationService.update(request, EXISTS_ID, LocalDateTime.MAX)) .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorCode.DUPLICATE_RESERVATION.getMessage()); + .hasMessage(ErrorCode.PAST_RESERVATION_NOT_ALLOWED.getMessage()); } } @@ -215,30 +185,27 @@ class Cancel { void 정상_취소시_삭제된다() { given(reservationRepository.findById(EXISTS_ID)) .willReturn(Optional.of(RoomEscapeFixture.reservation().build())); - given(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(anyLong(), anyLong(), any())) - .willReturn(Optional.empty()); + given(reservationRepository.findFirstWaitingBySlot(any())).willReturn(Optional.empty()); - assertThatCode(() -> reservationService.cancel(EXISTS_ID, TODAY)).doesNotThrowAnyException(); - verify(reservationRepository).deleteById(EXISTS_ID); + assertThatCode(() -> reservationService.cancel(EXISTS_ID, NOW)).doesNotThrowAnyException(); } @Test void APPROVED_예약_취소_시_첫번째_WAITING_예약이_승격된다() { - Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자").slot(DUMMY_SLOT) - .status(Status.WAITING).createdAt(TODAY).build(); - given(reservationRepository.findById(1L)).willReturn(Optional.of(DUMMY)); - given(reservationRepository.findFirstWaitingByTimeAndThemeAndDate(anyLong(), anyLong(), any())) - .willReturn(Optional.of(waiting)); + Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자") + .status(Status.WAITING).createdAt(NOW).build(); + given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); + given(reservationRepository.findFirstWaitingBySlot(any())).willReturn(Optional.of(waiting)); - reservationService.cancel(1L, LocalDateTime.MIN); + reservationService.cancel(EXISTS_ID, LocalDateTime.MIN); verify(reservationRepository).updateStatus(2L, Status.APPROVED); } @Test void WAITING_예약_취소_시_승격이_발생하지_않는다() { - Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자").slot(DUMMY_SLOT) - .status(Status.WAITING).createdAt(TODAY).build(); + Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자") + .status(Status.WAITING).createdAt(NOW).build(); given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); reservationService.cancel(2L, LocalDateTime.MIN); @@ -249,9 +216,9 @@ class Cancel { @Test void 존재하지_않는_예약_취소시_예외_발생() { - given(reservationRepository.findById(999L)).willReturn(Optional.empty()); + given(reservationRepository.findById(NOT_EXISTS_ID)).willReturn(Optional.empty()); - Assertions.assertThatThrownBy(() -> reservationService.cancel(999L, LocalDateTime.MIN)) + Assertions.assertThatThrownBy(() -> reservationService.cancel(NOT_EXISTS_ID, NOW)) .isInstanceOf(RoomEscapeException.class); } } diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java index 753ae8dc7e..ca5880aca4 100644 --- a/src/test/java/roomescape/service/ThemeServiceTest.java +++ b/src/test/java/roomescape/service/ThemeServiceTest.java @@ -60,12 +60,6 @@ class ThemeServiceTest { verify(themeRepository).findFamous(any()); } - @Test - void 삭제시_테마가_존재하지_않으면_예외가_발생한다() { - given(themeRepository.existsById(999L)).willReturn(false); - Assertions.assertThatThrownBy(() -> themeService.delete(999L)).isInstanceOf(RoomEscapeException.class); - } - @Test void 삭제시_테마를_사용하는_예외가_있으면_예외가_발생한다() { given(themeRepository.existsById(1L)).willReturn(true); From 16bdcd59eafeacb90343e32a6855f4055dea0e81 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Fri, 5 Jun 2026 13:14:53 +0900 Subject: [PATCH 11/26] =?UTF-8?q?refactor:=20RankedReservation=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=EC=A0=9C=EC=96=B4=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/RankedReservation.java | 2 +- src/test/java/roomescape/RoomEscapeFixture.java | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/RankedReservation.java b/src/main/java/roomescape/domain/reservation/RankedReservation.java index 2d23dbd024..c938bd7800 100644 --- a/src/main/java/roomescape/domain/reservation/RankedReservation.java +++ b/src/main/java/roomescape/domain/reservation/RankedReservation.java @@ -6,7 +6,7 @@ public class RankedReservation { private final Rank rank; private final Reservation reservation; - public RankedReservation(Rank rank, Reservation reservation) { + private RankedReservation(Rank rank, Reservation reservation) { this.rank = rank; this.reservation = reservation; } diff --git a/src/test/java/roomescape/RoomEscapeFixture.java b/src/test/java/roomescape/RoomEscapeFixture.java index 342196af76..9db92745bd 100644 --- a/src/test/java/roomescape/RoomEscapeFixture.java +++ b/src/test/java/roomescape/RoomEscapeFixture.java @@ -8,8 +8,6 @@ import roomescape.controller.dto.request.ReservationCreateRequest; import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.controller.dto.request.ThemeFamousFindRequest; -import roomescape.domain.reservation.Rank; -import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationName; @@ -33,8 +31,6 @@ public class RoomEscapeFixture { static final ReservationDate PAST_DATE = new ReservationDate(PAST_DATE_TIME.toLocalDate()); static final ReservationTime TIME = ReservationTime.of(LocalTime.of(10, 0)); static final Theme THEME = Theme.create(new ThemeName("공포"), "무서워요", new ThumbnailUrl("https://zeze.com")); - private static final Rank APPROVE_RANK = new Rank(1); - private static final Rank WAITING_RANK = new Rank(2); public static SlotBuilder slot() { return new SlotBuilder(); @@ -48,14 +44,6 @@ public static Theme theme() { return THEME; } - public static RankedReservation reservationResultWithApproved() { - return new RankedReservation(APPROVE_RANK, reservation().build()); - } - - public static RankedReservation reservationResultWithWaiting() { - return new RankedReservation(WAITING_RANK, reservation().id(2L).status(Status.WAITING).build()); - } - public static ThemeFamousFindRequest themeFamousFindRequest() { return new ThemeFamousFindRequest(7L, FUTURE_DATE.getValue(), 10L); } From d76788013569b5d9c1480368fb3d711926f3e10b Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Fri, 5 Jun 2026 13:15:43 +0900 Subject: [PATCH 12/26] =?UTF-8?q?refactor:=20SlotService=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/service/SlotService.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/java/roomescape/service/SlotService.java b/src/main/java/roomescape/service/SlotService.java index ce66562aeb..9ec8ed89db 100644 --- a/src/main/java/roomescape/service/SlotService.java +++ b/src/main/java/roomescape/service/SlotService.java @@ -28,18 +28,6 @@ public SlotService(SlotRepository slotRepository, ReservationTimeRepository rese this.themeRepository = themeRepository; } - @Transactional - public Slot create(LocalDate date, long timeId, long themeId) { - ReservationTime time = reservationTimeRepository.findById(timeId) - .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); - Theme theme = themeRepository.findById(themeId) - .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); - - return slotRepository.findByDateAndTimeAndTheme(date, time, theme) - .orElseThrow(() -> new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND)); - - } - @Transactional public Slot findOrCreate(LocalDate date, long timeId, long themeId) { ReservationTime time = reservationTimeRepository.findById(timeId) From 25e6e89de84182dc9122195873fafd97527c7844 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Sun, 7 Jun 2026 11:28:05 +0900 Subject: [PATCH 13/26] =?UTF-8?q?refactor:=20Reservation=20ensureNotPast?= =?UTF-8?q?=20tda=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/Reservation.java | 4 +--- .../roomescape/domain/reservation/Slot.java | 7 +++++++ .../java/roomescape/RoomEscapeFixture.java | 10 +++++----- .../domain/reservation/ReservationTest.java | 18 ++++++++++++++++++ .../domain/reservation/SlotTest.java | 17 +++++++++++++++++ 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 23efa69e62..f70e79107a 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -34,9 +34,7 @@ public static Reservation create(ReservationName reservationName, Slot slot, Sta } public void ensureNotPast(LocalDateTime now) { - LocalDateTime requestDateTime = LocalDateTime.of(slot.getDate().getValue(), slot.getTime().getStartAt()); - - if (requestDateTime.isBefore(now)) { + if (slot.isBefore(now)) { throw new RoomEscapeException(ErrorCode.PAST_RESERVATION_NOT_ALLOWED); } } diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index f5d89f8e36..b9d72301df 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -1,5 +1,6 @@ package roomescape.domain.reservation; +import java.time.LocalDateTime; import java.util.Objects; import roomescape.domain.theme.Theme; @@ -32,6 +33,12 @@ public boolean isSame(Reservation target) { return id == target.getSlot().getId(); } + public boolean isBefore(LocalDateTime now) { + LocalDateTime reservationDateTime = LocalDateTime.of(date.getValue(), time.getStartAt()); + + return reservationDateTime.isBefore(now); + } + public long getId() { return id; } diff --git a/src/test/java/roomescape/RoomEscapeFixture.java b/src/test/java/roomescape/RoomEscapeFixture.java index 9db92745bd..1bd7c5145d 100644 --- a/src/test/java/roomescape/RoomEscapeFixture.java +++ b/src/test/java/roomescape/RoomEscapeFixture.java @@ -26,11 +26,11 @@ public class RoomEscapeFixture { public static final LocalDateTime PAST_DATE_TIME = LocalDateTime.of(2000, 11, 11, 10, 0); public static final LocalDateTime FUTURE_DATE_TIME = LocalDateTime.of(2099, 11, 11, 10, 0); - static final ReservationName NAME = new ReservationName("zeze"); - static final ReservationDate FUTURE_DATE = new ReservationDate(FUTURE_DATE_TIME.toLocalDate()); - static final ReservationDate PAST_DATE = new ReservationDate(PAST_DATE_TIME.toLocalDate()); - static final ReservationTime TIME = ReservationTime.of(LocalTime.of(10, 0)); - static final Theme THEME = Theme.create(new ThemeName("공포"), "무서워요", new ThumbnailUrl("https://zeze.com")); + public static final ReservationName NAME = new ReservationName("zeze"); + public static final ReservationDate FUTURE_DATE = new ReservationDate(FUTURE_DATE_TIME.toLocalDate()); + public static final ReservationDate PAST_DATE = new ReservationDate(PAST_DATE_TIME.toLocalDate()); + public static final ReservationTime TIME = ReservationTime.of(LocalTime.of(10, 0)); + public static final Theme THEME = Theme.create(new ThemeName("공포"), "무서워요", new ThumbnailUrl("https://zeze.com")); public static SlotBuilder slot() { return new SlotBuilder(); diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 337150ed53..29f909bc11 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import common.exception.RoomEscapeException; import java.time.LocalDateTime; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -47,4 +48,21 @@ static Stream nullCases() { Assertions.assertThat(id1WithSameDate.isEarlierThan(id2WithSameDate)).isFalse(); } + + @Test + void 예약_일정이_제공된_시점보다_과거라면_예외가_발생한다() { + Slot slot = RoomEscapeFixture.slot().date(RoomEscapeFixture.PAST_DATE).build(); + Reservation past = RoomEscapeFixture.reservation().slot(slot).build(); + + Assertions.assertThatThrownBy(() -> past.ensureNotPast(LocalDateTime.now())) + .isInstanceOf(RoomEscapeException.class); + } + + @Test + void 예약_일정이_제공된_시점보다_미래라면_예외가_발생하지_않는다() { + Slot slot = RoomEscapeFixture.slot().date(RoomEscapeFixture.FUTURE_DATE).build(); + Reservation future = RoomEscapeFixture.reservation().slot(slot).build(); + + Assertions.assertThatCode(() -> future.ensureNotPast(LocalDateTime.now())); + } } diff --git a/src/test/java/roomescape/domain/reservation/SlotTest.java b/src/test/java/roomescape/domain/reservation/SlotTest.java index d67550ad03..b5ad6af2d0 100644 --- a/src/test/java/roomescape/domain/reservation/SlotTest.java +++ b/src/test/java/roomescape/domain/reservation/SlotTest.java @@ -3,8 +3,11 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -30,4 +33,18 @@ static Stream nullCases() { Arguments.of(date, time, null) ); } + + @Test + void 제공된_시점보다_과거의_슬롯이면_true를_반환한다() { + Slot past = RoomEscapeFixture.slot().date(RoomEscapeFixture.PAST_DATE).build(); + + Assertions.assertThat(past.isBefore(LocalDateTime.now())).isTrue(); + } + + @Test + void 제공된_시점보다_미래의_슬롯이면_true를_반환한다() { + Slot future = RoomEscapeFixture.slot().date(RoomEscapeFixture.FUTURE_DATE).build(); + + Assertions.assertThat(future.isBefore(LocalDateTime.now())).isFalse(); + } } From ab706a01edccac294ec41e25b3b9035923cc2b04 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Sun, 7 Jun 2026 14:39:25 +0900 Subject: [PATCH 14/26] =?UTF-8?q?refactor:=20Reservation=20Service=20?= =?UTF-8?q?=ED=9D=90=EB=A6=84=EC=9D=84=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/RankedReservations.java | 39 ------------------- .../domain/reservation/Reservation.java | 24 ++++-------- .../domain/reservation/ReservationTime.java | 14 ------- .../domain/reservation/Reservations.java | 37 ++++++++++++++++-- .../roomescape/domain/reservation/Slot.java | 4 +- .../roomescape/domain/reservation/Status.java | 4 ++ .../java/roomescape/domain/theme/Theme.java | 15 ------- .../service/ReservationService.java | 26 +++++-------- .../reservation/RankedReservationsTest.java | 38 ------------------ .../domain/reservation/ReservationTest.java | 4 +- .../domain/reservation/ReservationsTest.java | 29 ++++++++++++++ 11 files changed, 86 insertions(+), 148 deletions(-) delete mode 100644 src/main/java/roomescape/domain/reservation/RankedReservations.java delete mode 100644 src/test/java/roomescape/domain/reservation/RankedReservationsTest.java diff --git a/src/main/java/roomescape/domain/reservation/RankedReservations.java b/src/main/java/roomescape/domain/reservation/RankedReservations.java deleted file mode 100644 index 1bbe86cacc..0000000000 --- a/src/main/java/roomescape/domain/reservation/RankedReservations.java +++ /dev/null @@ -1,39 +0,0 @@ -package roomescape.domain.reservation; - -import java.util.List; - -public class RankedReservations { - private final List reservations; - - public RankedReservations(List reservations) { - this.reservations = reservations; - } - - public List resultsOf(String name) { - List listByName = getListByName(name); - - return listByName.stream() - .map(this::toRankedReservation) - .toList(); - } - - private RankedReservation toRankedReservation(Reservation target) { - List sameSlots = reservations.stream() - .filter(reservation -> reservation.isSameSlot(target)) - .toList(); - - return RankedReservation.decideRankFrom(target, sameSlots); - } - - private List getListByName(String name) { - return reservations.stream() - .filter(reservation -> reservation.getName().equals(new ReservationName(name))) - .toList(); - } - - public List resultsOf() { - return reservations.stream() - .map(this::toRankedReservation) - .toList(); - } -} diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index f70e79107a..eba9819a6c 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -29,11 +29,11 @@ public static Reservation load(long id, ReservationName reservationName, Slot sl public static Reservation create(ReservationName reservationName, Slot slot, Status status, LocalDateTime now) { Objects.requireNonNull(now); Reservation reservation = new Reservation(0L, reservationName, slot, status, now); - reservation.ensureNotPast(now); + reservation.isPastFrom(now); return reservation; } - public void ensureNotPast(LocalDateTime now) { + public void isPastFrom(LocalDateTime now) { if (slot.isBefore(now)) { throw new RoomEscapeException(ErrorCode.PAST_RESERVATION_NOT_ALLOWED); } @@ -47,10 +47,14 @@ public boolean isEarlierThan(Reservation target) { return id < target.getId(); } - public boolean isSameSlot(Reservation target) { + public boolean isSameSlot(Slot target) { return slot.isSame(target); } + public boolean isApproved() { + return status.isApproved(); + } + public Reservation withId(long id) { return new Reservation(id, name, slot, status, createdAt); } @@ -86,18 +90,4 @@ public Status getStatus() { public LocalDateTime getCreatedAt() { return createdAt; } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - Reservation that = (Reservation) o; - return id == that.id && Objects.equals(name, that.name) && Objects.equals(slot, that.slot); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, slot); - } } diff --git a/src/main/java/roomescape/domain/reservation/ReservationTime.java b/src/main/java/roomescape/domain/reservation/ReservationTime.java index e0fbf3ee69..2f3e9041de 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservation/ReservationTime.java @@ -31,18 +31,4 @@ public long getId() { public LocalTime getStartAt() { return startAt; } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - ReservationTime that = (ReservationTime) o; - return id == that.id && Objects.equals(startAt, that.startAt); - } - - @Override - public int hashCode() { - return Objects.hash(id, startAt); - } } diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java index 37301d42ed..b8807745a3 100644 --- a/src/main/java/roomescape/domain/reservation/Reservations.java +++ b/src/main/java/roomescape/domain/reservation/Reservations.java @@ -13,14 +13,14 @@ public Reservations(List reservations) { } public Reservation reserve(ReservationName reservationName, Slot foundSlot, LocalDateTime now) { - validateHasName(reservationName, foundSlot); + validateNoDuplicate(reservationName, foundSlot); - Status status = getStatus(); + Status status = decideStatusFor(foundSlot); return Reservation.create(reservationName, foundSlot, status, now); } - private void validateHasName(ReservationName reservationName, Slot slot) { + private void validateNoDuplicate(ReservationName reservationName, Slot slot) { if (reservations.stream() .filter(reservation -> reservation.getSlot().equals(slot)) .anyMatch(reservation -> reservation.getName().equals(reservationName))) { @@ -28,11 +28,40 @@ private void validateHasName(ReservationName reservationName, Slot slot) { } } - private Status getStatus() { + private Status decideStatusFor(Slot slot) { if (reservations.stream() + .filter(reservation -> reservation.isSameSlot(slot)) .anyMatch(reservation -> reservation.getStatus().equals(Status.APPROVED))) { return Status.WAITING; } return Status.APPROVED; } + + public List rankedReservationsOf(String name) { + List listByName = findByName(name); + + return listByName.stream() + .map(this::toRankedReservation) + .toList(); + } + + private List findByName(String name) { + return reservations.stream() + .filter(reservation -> reservation.getName().equals(new ReservationName(name))) + .toList(); + } + + public List allRankedReservationsOf() { + return reservations.stream() + .map(this::toRankedReservation) + .toList(); + } + + private RankedReservation toRankedReservation(Reservation target) { + List sameSlots = reservations.stream() + .filter(reservation -> reservation.isSameSlot(target.getSlot())) + .toList(); + + return RankedReservation.decideRankFrom(target, sameSlots); + } } diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index b9d72301df..7f63b43879 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -29,8 +29,8 @@ public Slot withId(long id) { return new Slot(id, date, time, theme); } - public boolean isSame(Reservation target) { - return id == target.getSlot().getId(); + public boolean isSame(Slot target) { + return id == target.id; } public boolean isBefore(LocalDateTime now) { diff --git a/src/main/java/roomescape/domain/reservation/Status.java b/src/main/java/roomescape/domain/reservation/Status.java index 1df323f658..da3a8b5b34 100644 --- a/src/main/java/roomescape/domain/reservation/Status.java +++ b/src/main/java/roomescape/domain/reservation/Status.java @@ -13,4 +13,8 @@ public enum Status { public String getKoreanName() { return koreanName; } + + public boolean isApproved() { + return this == APPROVED; + } } diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index 8dbd21f12b..5c8c99f58e 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -42,19 +42,4 @@ public String getDescription() { public ThumbnailUrl getThumbnailUrl() { return thumbnailUrl; } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - Theme theme = (Theme) o; - return id == theme.id && Objects.equals(name, theme.name) && Objects.equals(description, - theme.description) && Objects.equals(thumbnailUrl, theme.thumbnailUrl); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, description, thumbnailUrl); - } } diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index a8b2a60f73..1b38eb5159 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -9,7 +9,6 @@ import roomescape.controller.dto.request.ReservationCreateRequest; import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.domain.reservation.RankedReservation; -import roomescape.domain.reservation.RankedReservations; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.Reservations; @@ -48,30 +47,30 @@ public RankedReservation find(long reservationId) { } public List findList(String name) { - RankedReservations rankedReservations = new RankedReservations(reservationRepository.findAll()); + Reservations rankedReservations = new Reservations(reservationRepository.findAll()); if (name == null) { - return rankedReservations.resultsOf(); + return rankedReservations.allRankedReservationsOf(); } - return rankedReservations.resultsOf(name); + return rankedReservations.rankedReservationsOf(name); } @Transactional public RankedReservation update(ReservationUpdateRequest request, long id, LocalDateTime now) { Reservation originReservation = findReservationById(id); - originReservation.ensureNotPast(now); + originReservation.isPastFrom(now); Slot updateSlot = slotService.findOrCreate(request.getDate(), request.getTimeId(), request.getThemeId()); slotService.lockSlot(updateSlot); - validateIsDuplicateReservation(updateSlot, request.getName()); - Reservations reservations = new Reservations(reservationRepository.findAllBySlot(updateSlot)); Reservation reserved = reservations.reserve(new ReservationName(request.getName()), updateSlot, now); Reservation updated = reservationRepository.update(id, reserved); - findFirstWaitingAndUpdateStatus(originReservation); + if (originReservation.isApproved()) { + findFirstWaitingAndUpdateStatus(originReservation); + } return getRankedReservation(updated); } @@ -84,22 +83,15 @@ private void findFirstWaitingAndUpdateStatus(Reservation reservation) { @Transactional public void cancel(long reservationId, LocalDateTime now) { Reservation reservation = findReservationById(reservationId); - reservation.ensureNotPast(now); + reservation.isPastFrom(now); - Status cancelledStatus = reservation.getStatus(); reservationRepository.deleteById(reservationId); - if (cancelledStatus == Status.APPROVED) { + if (reservation.isApproved()) { findFirstWaitingAndUpdateStatus(reservation); } } - private void validateIsDuplicateReservation(Slot slot, String name) { - if (reservationRepository.existsBySlotAndName(slot, name)) { - throw new RoomEscapeException(ErrorCode.DUPLICATE_RESERVATION); - } - } - private Reservation findReservationById(long reservationId) { return reservationRepository.findById(reservationId).orElseThrow( () -> new RoomEscapeException(ErrorCode.RESERVATION_NOT_FOUND)); diff --git a/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java b/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java deleted file mode 100644 index b861cab58c..0000000000 --- a/src/test/java/roomescape/domain/reservation/RankedReservationsTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package roomescape.domain.reservation; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.LocalDateTime; -import java.util.List; -import org.junit.jupiter.api.Test; -import roomescape.RoomEscapeFixture; - -class RankedReservationsTest { - @Test - void 같은_슬롯에서_먼저_예약한_사람이_rank_0이다() { - Reservation first = RoomEscapeFixture.reservation() - .name("제제").createdAt(LocalDateTime.of(2099, 1, 1, 9, 0)).build(); - Reservation second = RoomEscapeFixture.reservation() - .id(2L).name("달수").status(Status.WAITING).createdAt(LocalDateTime.of(2099, 1, 1, 9, 1)).build(); - - RankedReservations rankedReservations = new RankedReservations(List.of(first, second)); - List results = rankedReservations.resultsOf(); - - assertThat(results.get(0).getRank().getValue()).isEqualTo(0); - assertThat(results.get(1).getRank().getValue()).isEqualTo(1); - } - - @Test - void 이름으로_조회하면_해당_이름의_예약만_반환된다() { - Reservation r1 = RoomEscapeFixture.reservation() - .name("제제").createdAt(LocalDateTime.of(2099, 1, 1, 9, 0)).build(); - Reservation r2 = RoomEscapeFixture.reservation() - .id(2L).name("달수").status(Status.WAITING).createdAt(LocalDateTime.of(2099, 1, 1, 9, 1)).build(); - - RankedReservations rankedReservations = new RankedReservations(List.of(r1, r2)); - List results = rankedReservations.resultsOf("달수"); - - assertThat(results).hasSize(1); - assertThat(results.getFirst().getReservation()).isEqualTo(r2); - } -} diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 29f909bc11..5420920d6a 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -54,7 +54,7 @@ static Stream nullCases() { Slot slot = RoomEscapeFixture.slot().date(RoomEscapeFixture.PAST_DATE).build(); Reservation past = RoomEscapeFixture.reservation().slot(slot).build(); - Assertions.assertThatThrownBy(() -> past.ensureNotPast(LocalDateTime.now())) + Assertions.assertThatThrownBy(() -> past.isPastFrom(LocalDateTime.now())) .isInstanceOf(RoomEscapeException.class); } @@ -63,6 +63,6 @@ static Stream nullCases() { Slot slot = RoomEscapeFixture.slot().date(RoomEscapeFixture.FUTURE_DATE).build(); Reservation future = RoomEscapeFixture.reservation().slot(slot).build(); - Assertions.assertThatCode(() -> future.ensureNotPast(LocalDateTime.now())); + Assertions.assertThatCode(() -> future.isPastFrom(LocalDateTime.now())); } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationsTest.java b/src/test/java/roomescape/domain/reservation/ReservationsTest.java index b10636a719..922f0902d1 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationsTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationsTest.java @@ -62,4 +62,33 @@ class ReservationsTest { assertThat(result.getName()).isEqualTo(NAME); } + + + @Test + void 같은_슬롯에서_먼저_예약한_사람이_rank_0이다() { + Reservation first = RoomEscapeFixture.reservation() + .name("제제").createdAt(LocalDateTime.of(2099, 1, 1, 9, 0)).build(); + Reservation second = RoomEscapeFixture.reservation() + .id(2L).name("달수").status(Status.WAITING).createdAt(LocalDateTime.of(2099, 1, 1, 9, 1)).build(); + + Reservations rankedReservations = new Reservations(List.of(first, second)); + List results = rankedReservations.allRankedReservationsOf(); + + assertThat(results.get(0).getRank().getValue()).isEqualTo(0); + assertThat(results.get(1).getRank().getValue()).isEqualTo(1); + } + + @Test + void 이름으로_조회하면_해당_이름의_예약만_반환된다() { + Reservation r1 = RoomEscapeFixture.reservation() + .name("제제").createdAt(LocalDateTime.of(2099, 1, 1, 9, 0)).build(); + Reservation r2 = RoomEscapeFixture.reservation() + .id(2L).name("달수").status(Status.WAITING).createdAt(LocalDateTime.of(2099, 1, 1, 9, 1)).build(); + + Reservations rankedReservations = new Reservations(List.of(r1, r2)); + List results = rankedReservations.rankedReservationsOf("달수"); + + assertThat(results).hasSize(1); + assertThat(results.getFirst().getReservation()).isEqualTo(r2); + } } From 34230f89d8b9b11b4d56c90ad5ced682e4036dbf Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Sun, 7 Jun 2026 14:47:39 +0900 Subject: [PATCH 15/26] =?UTF-8?q?refactor:=20Reservations=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20null=20=EA=B2=80=EC=82=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/domain/reservation/Reservations.java | 3 ++- .../roomescape/domain/reservation/ReservationsTest.java | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java index b8807745a3..80a3a6f8eb 100644 --- a/src/main/java/roomescape/domain/reservation/Reservations.java +++ b/src/main/java/roomescape/domain/reservation/Reservations.java @@ -4,12 +4,13 @@ import common.exception.RoomEscapeException; import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; public class Reservations { private final List reservations; public Reservations(List reservations) { - this.reservations = reservations; + this.reservations = Objects.requireNonNull(reservations); } public Reservation reserve(ReservationName reservationName, Slot foundSlot, LocalDateTime now) { diff --git a/src/test/java/roomescape/domain/reservation/ReservationsTest.java b/src/test/java/roomescape/domain/reservation/ReservationsTest.java index 922f0902d1..d4a8bf47fa 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationsTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationsTest.java @@ -6,6 +6,7 @@ import common.exception.RoomEscapeException; import java.time.LocalDateTime; import java.util.List; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import roomescape.RoomEscapeFixture; @@ -14,6 +15,11 @@ class ReservationsTest { private static final ReservationName NAME = new ReservationName("zeze"); private static final LocalDateTime NOW = RoomEscapeFixture.PAST_DATE_TIME; + @Test + void null로_생성되면_예외가_발생한다() { + Assertions.assertThatThrownBy(() -> new Reservations(null)).isInstanceOf(NullPointerException.class); + } + @Test void 예약이_없으면_APPROVED로_생성된다() { Reservations reservations = new Reservations(List.of()); From 84cc19bf74c660d03ae48f9daa282f5fb781e3ff Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Sun, 7 Jun 2026 15:27:22 +0900 Subject: [PATCH 16/26] =?UTF-8?q?test:=20ReservationService=20update?= =?UTF-8?q?=EC=8B=9C=20WAITING=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=EB=A7=8C=20?= =?UTF-8?q?=EC=8A=B9=EA=B2=A9=20TC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReservationServiceTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index 4ab8d439e1..e29e5ca4e4 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import common.exception.ErrorCode; @@ -175,6 +176,28 @@ class Update { .isInstanceOf(RoomEscapeException.class) .hasMessage(ErrorCode.PAST_RESERVATION_NOT_ALLOWED.getMessage()); } + + @Test + void WAITING_예약을_수정하면_승격이_발생하지_않는다() { + Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자") + .status(Status.WAITING).createdAt(NOW).build(); + Reservation updated = RoomEscapeFixture.reservation().id(2L).name("대기자") + .status(Status.WAITING).createdAt(NOW).build(); + + given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); + given(slotService.findOrCreate(any(), anyLong(), anyLong())).willReturn(SLOT); + given(reservationRepository.findAllBySlot(any())) + .willReturn(List.of()) + .willReturn(List.of(updated)); + given(reservationRepository.update(anyLong(), any())).willReturn(updated); + + ReservationUpdateRequest request = new ReservationUpdateRequest("대기자", + LocalDate.of(2099, 11, 11), 1L, 1L); + + reservationService.update(request, 2L, LocalDateTime.MIN); + + verify(reservationRepository, times(0)).updateStatus(anyLong(), any()); + } } @Nested From ae8b3eec23366c434b572d7125de41dec0c5c291 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 11 Jun 2026 14:53:08 +0900 Subject: [PATCH 17/26] =?UTF-8?q?test:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20Mockit?= =?UTF-8?q?o=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationServiceIntegrationTest.java | 8 +- .../service/ReservationServiceTest.java | 373 ++++++++++-------- 2 files changed, 202 insertions(+), 179 deletions(-) diff --git a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java index 05b52e30c5..499a2e8e0f 100644 --- a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java +++ b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import io.restassured.RestAssured; import java.time.LocalDateTime; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; @@ -12,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; import roomescape.RoomEscapeFixture; @@ -21,7 +19,7 @@ import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.Status; -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) class ReservationServiceIntegrationTest { @Autowired @@ -30,12 +28,8 @@ class ReservationServiceIntegrationTest { @Autowired private JdbcTemplate jdbcTemplate; - @LocalServerPort - int port; - @BeforeEach void init() { - RestAssured.port = port; jdbcTemplate.update("insert into reservation_time(start_at) values ('10:00')"); jdbcTemplate.update( "insert into theme(name, description, thumbnail_url) values ('공포', '무서워요', 'https://zeze.com')"); diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index e29e5ca4e4..72ce64e10d 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -1,248 +1,277 @@ package roomescape.service; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import common.exception.ErrorCode; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import common.exception.RoomEscapeException; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.transaction.annotation.Transactional; import roomescape.RoomEscapeFixture; import roomescape.controller.dto.request.ReservationCreateRequest; import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationDate; +import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; +import roomescape.domain.theme.Theme; import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.SlotRepository; +import roomescape.repository.ThemeRepository; + +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@Transactional +public class ReservationServiceTest { + @Autowired + private ReservationService reservationService; -@ExtendWith(MockitoExtension.class) -class ReservationServiceTest { - private static final String NAME = "제제"; - private static final LocalDateTime NOW = LocalDateTime.of(2026, 5, 10, 10, 0, 0); - private static final Slot SLOT = RoomEscapeFixture.slot().build(); - private static final Reservation DUMMY = RoomEscapeFixture.reservation().name(NAME).createdAt(NOW).build(); - private static final long NOT_EXISTS_ID = Long.MAX_VALUE; - private static final long EXISTS_ID = 1L; - - @Mock + @Autowired private ReservationRepository reservationRepository; - - @Mock - private SlotService slotService; - - @InjectMocks - private ReservationService reservationService; + @Autowired + private ThemeRepository themeRepository; + @Autowired + private ReservationTimeRepository reservationTimeRepository; + @Autowired + private SlotRepository slotRepository; @Nested - @DisplayName("reserve") + @DisplayName("예약 생성시") class Reserve { - @Test - void 슬롯_조회_실패시_예외가_발생한다() { - given(slotService.findOrCreate(any(), anyLong(), anyLong())) - .willThrow(new RoomEscapeException(ErrorCode.RESERVATION_TIME_NOT_FOUND)); - - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequest(); - - Assertions.assertThatThrownBy(() -> reservationService.reserve(request, NOW)) - .isInstanceOf(RoomEscapeException.class); - } + @DisplayName("이미 승인 예약이 있는 슬롯에 예약하면 대기로 생성된다") + void waiting_when_slot_already_has_approved() { + Slot slot = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, + RoomEscapeFixture.FUTURE_DATE); + String name = "zeze"; + saveReservation(slot, name, Status.APPROVED); - @Test - void 슬롯에_예약이_없으면_APPROVED로_예약된다() { - given(slotService.findOrCreate(any(), anyLong(), anyLong())).willReturn(SLOT); - given(reservationRepository.findAllBySlot(any())) - .willReturn(List.of()) - .willReturn(List.of(DUMMY)); - given(reservationRepository.save(any())).willReturn(DUMMY); + String newCustomerName = "dalsu"; - ReservationCreateRequest request = new ReservationCreateRequest(NAME, - LocalDate.of(2099, 11, 11), 1L, 1L); + ReservationCreateRequest request = createRequestTo(newCustomerName, slot); - RankedReservation result = reservationService.reserve(request, NOW); + RankedReservation reserved = reservationService.reserve(request, LocalDateTime.now()); - Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.APPROVED); + Reservation saved = reservationRepository.findById(reserved.getReservation().getId()).get(); + assertThat(saved.getStatus()).isEqualTo(Status.WAITING); + assertThat(saved.getName().getValue()).isEqualTo(newCustomerName); } @Test - void APPROVED가_이미_있으면_WAITING으로_예약된다() { - Reservation existing = RoomEscapeFixture.reservation().name("기존").createdAt(NOW).build(); - Reservation waitingSaved = RoomEscapeFixture.reservation().id(2L).name("달수") - .status(Status.WAITING).createdAt(NOW).build(); + @DisplayName("같은 슬롯에 같은 이름으로 예약하면 예외가 발생한다") + void reject_duplicate_name_in_same_slot() { + Slot slot = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, RoomEscapeFixture.FUTURE_DATE); + String duplicatedName = "zeze"; - given(slotService.findOrCreate(any(), anyLong(), anyLong())).willReturn(SLOT); - given(reservationRepository.findAllBySlot(any())) - .willReturn(List.of(existing)) - .willReturn(List.of(existing, waitingSaved)); - given(reservationRepository.save(any())).willReturn(waitingSaved); + saveReservation(slot, duplicatedName, Status.APPROVED); - ReservationCreateRequest request = new ReservationCreateRequest("달수", - LocalDate.of(2099, 11, 11), 1L, 1L); + ReservationCreateRequest request = createRequestTo(duplicatedName, slot); - RankedReservation result = reservationService.reserve(request, NOW); + assertThatThrownBy(() -> reservationService.reserve(request, LocalDateTime.now())).isInstanceOf( + RoomEscapeException.class); + } - Assertions.assertThat(result.getReservation().getStatus()).isEqualTo(Status.WAITING); + private static ReservationCreateRequest createRequestTo(String name, Slot slot) { + return new ReservationCreateRequest(name, + slot.getDate().getValue(), slot.getTime().getId(), + slot.getTheme().getId()); } } @Nested - @DisplayName("find") - class Find { - + @DisplayName("예약 삭제시") + class Cancel { @Test - void 존재하는_ID면_결과를_반환한다() { - given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); - given(reservationRepository.findAllBySlot(any())).willReturn(List.of(DUMMY)); - - RankedReservation result = reservationService.find(EXISTS_ID); - - Assertions.assertThat(result.getReservation().getId()).isEqualTo(EXISTS_ID); - Assertions.assertThat(result.getRank().getValue()).isZero(); + @DisplayName("ID를 찾을 수 없으면 예외가 발생한다") + void throw_when_id_not_found() { + assertThatThrownBy(() -> reservationService.cancel(999L, LocalDateTime.now())).isInstanceOf( + RoomEscapeException.class); } @Test - void 존재하지_않는_ID면_예외가_발생한다() { - given(reservationRepository.findById(NOT_EXISTS_ID)).willReturn(Optional.empty()); + @DisplayName("과거 예약을 삭제하면 예외가 발생한다") + void throw_when_cancel_past_reservation() { + Slot pastSlot = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, RoomEscapeFixture.PAST_DATE); + Reservation pastTarget = saveReservation(pastSlot, "zeze", Status.APPROVED); - Assertions.assertThatThrownBy(() -> reservationService.find(NOT_EXISTS_ID)) - .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorCode.RESERVATION_NOT_FOUND.getMessage()); + // when & then + assertThatThrownBy(() -> reservationService.cancel(pastTarget.getId(), LocalDateTime.MAX)) + .isInstanceOf(RoomEscapeException.class); } - } - - @Nested - @DisplayName("findList") - class FindList { @Test - void 이름_없이_목록_조회시_전체_예약을_반환한다() { - given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); - - List results = reservationService.findList(null); - - Assertions.assertThat(results).hasSize(1); + @DisplayName("승인 예약이 삭제되면 첫 대기 예약이 승격된다") + void promote_first_waiting_when_approved_canceled() { + // given + Slot slot = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, + RoomEscapeFixture.FUTURE_DATE); + Reservation approvedTarget = saveReservation(slot, "zeze", Status.APPROVED); + Reservation firstWaiting = saveReservation(slot, "dalsu", Status.WAITING); + + // when + reservationService.cancel(approvedTarget.getId(), LocalDateTime.MIN); + + // then + assertThat(reservationRepository.findById(approvedTarget.getId())).isNotPresent(); + assertThat(reservationRepository.findById(firstWaiting.getId()).get().getStatus()).isEqualTo( + Status.APPROVED); } @Test - void 이름으로_목록_조회시_해당_이름의_예약만_반환한다() { - given(reservationRepository.findAll()).willReturn(List.of(DUMMY)); - - List results = reservationService.findList(NAME); - - Assertions.assertThat(results).hasSize(1); - Assertions.assertThat(results.getFirst().getReservation().getName().getValue()).isEqualTo(NAME); + @DisplayName("대기 예약이 삭제되면 남은 대기 예약은 그대로여야 한다") + void keep_remaining_waiting_when_waiting_canceled() { + // given + Slot slot = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, + RoomEscapeFixture.FUTURE_DATE); + saveReservation(slot, "zeze", Status.APPROVED); + + Reservation canceledWaiting = saveReservation(slot, "dalsu", Status.WAITING); + Reservation remainingWaiting = saveReservation(slot, "mingu", Status.WAITING); + + // when + reservationService.cancel(canceledWaiting.getId(), LocalDateTime.MIN); + + // then + assertThat(reservationRepository.findById(canceledWaiting.getId())).isNotPresent(); + assertThat(reservationRepository.findById(remainingWaiting.getId()).get().getStatus()).isEqualTo( + Status.WAITING); } } @Nested - @DisplayName("update") + @DisplayName("예약 수정시") class Update { @Test - void ID가_없으면_예외가_발생한다() { - given(reservationRepository.findById(NOT_EXISTS_ID)).willReturn(Optional.empty()); - - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", - LocalDate.of(2099, 4, 6), 1L, 1L); - - Assertions.assertThatThrownBy(() -> reservationService.update(request, NOT_EXISTS_ID, NOW)) - .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorCode.RESERVATION_NOT_FOUND.getMessage()); - } - - @Test - void 과거_날짜의_예약이면_예외가_발생한다() { - given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); - - ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", - LocalDate.of(2000, 4, 6), 1L, 1L); - - Assertions.assertThatThrownBy(() -> reservationService.update(request, EXISTS_ID, LocalDateTime.MAX)) - .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorCode.PAST_RESERVATION_NOT_ALLOWED.getMessage()); + @DisplayName("ID를 찾을 수 없으면 예외가 발생한다") + void throw_when_id_not_found() { + ReservationUpdateRequest request = RoomEscapeFixture.reservationUpdateRequest(); + assertThatThrownBy(() -> reservationService.update(request, 999L, LocalDateTime.now())) + .isInstanceOf(RoomEscapeException.class); } @Test - void WAITING_예약을_수정하면_승격이_발생하지_않는다() { - Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자") - .status(Status.WAITING).createdAt(NOW).build(); - Reservation updated = RoomEscapeFixture.reservation().id(2L).name("대기자") - .status(Status.WAITING).createdAt(NOW).build(); + @DisplayName("과거 예약을 수정하면 예외가 발생한다") + void throw_when_update_past_reservation() { + // given + String approvedName = "zeze"; - given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); - given(slotService.findOrCreate(any(), anyLong(), anyLong())).willReturn(SLOT); - given(reservationRepository.findAllBySlot(any())) - .willReturn(List.of()) - .willReturn(List.of(updated)); - given(reservationRepository.update(anyLong(), any())).willReturn(updated); + Slot futureSlot = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, + RoomEscapeFixture.FUTURE_DATE); + Reservation approvedReservation = saveReservation(futureSlot, approvedName, Status.APPROVED); - ReservationUpdateRequest request = new ReservationUpdateRequest("대기자", - LocalDate.of(2099, 11, 11), 1L, 1L); + ReservationUpdateRequest request = RoomEscapeFixture.reservationUpdateRequest(); - reservationService.update(request, 2L, LocalDateTime.MIN); - - verify(reservationRepository, times(0)).updateStatus(anyLong(), any()); + // when & then + assertThatThrownBy( + () -> reservationService.update(request, approvedReservation.getId(), LocalDateTime.MAX)) + .isInstanceOf(RoomEscapeException.class); } - } - - @Nested - @DisplayName("cancel") - class Cancel { @Test - void 정상_취소시_삭제된다() { - given(reservationRepository.findById(EXISTS_ID)) - .willReturn(Optional.of(RoomEscapeFixture.reservation().build())); - given(reservationRepository.findFirstWaitingBySlot(any())).willReturn(Optional.empty()); + @DisplayName("승인된 예약이 변경되면 대기 중인 예약이 승격되어야 한다") + void promote_waiting_reservation_if_approved() { + // given + Slot origin = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, + RoomEscapeFixture.FUTURE_DATE); + String approvedName = "zeze"; + String waitingName = "dalsu"; + + Reservation approvedTarget = saveReservation(origin, approvedName, Status.APPROVED); + Reservation firstWaiting = saveReservation(origin, waitingName, Status.WAITING); + + ReservationUpdateRequest request = new ReservationUpdateRequest("zeze", LocalDate.MAX, + origin.getTime().getId(), + origin.getTheme().getId()); + + // when + RankedReservation updated = reservationService.update(request, approvedTarget.getId(), + LocalDateTime.now()); + + // then + assertThat(updated.getRank().getValue()).isEqualTo(0); + assertThat(updated.getReservation().getStatus()).isEqualTo(Status.APPROVED); + assertThat(reservationRepository.findById(firstWaiting.getId()).get().getStatus()) + .isEqualTo(Status.APPROVED); - assertThatCode(() -> reservationService.cancel(EXISTS_ID, NOW)).doesNotThrowAnyException(); } @Test - void APPROVED_예약_취소_시_첫번째_WAITING_예약이_승격된다() { - Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자") - .status(Status.WAITING).createdAt(NOW).build(); - given(reservationRepository.findById(EXISTS_ID)).willReturn(Optional.of(DUMMY)); - given(reservationRepository.findFirstWaitingBySlot(any())).willReturn(Optional.of(waiting)); - - reservationService.cancel(EXISTS_ID, LocalDateTime.MIN); - - verify(reservationRepository).updateStatus(2L, Status.APPROVED); + @DisplayName("대기 예약이 옮겨지면 남은 대기 예약은 그대로다") + void keep_remaining_waiting_when_waiting_moved_out() { + // given + Slot origin = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, + RoomEscapeFixture.FUTURE_DATE); + saveReservation(origin, "zeze", Status.APPROVED); + + String waitingName = "dalsu"; + String waitingName2 = "mingu"; + + Reservation waitingTarget = saveReservation(origin, waitingName, Status.WAITING); + Reservation remainingWaiting = saveReservation(origin, waitingName2, Status.WAITING); + + ReservationUpdateRequest request = new ReservationUpdateRequest(waitingName, LocalDate.MAX, + origin.getTime().getId(), + origin.getTheme().getId()); + + // when + RankedReservation updated = reservationService.update(request, waitingTarget.getId(), + LocalDateTime.now()); + + // then + assertThat(updated.getRank().getValue()).isEqualTo(0); + assertThat(updated.getReservation().getStatus()).isEqualTo(Status.APPROVED); + assertThat(reservationRepository.findById(remainingWaiting.getId()).get().getStatus()) + .isEqualTo(Status.WAITING); } @Test - void WAITING_예약_취소_시_승격이_발생하지_않는다() { - Reservation waiting = RoomEscapeFixture.reservation().id(2L).name("대기자") - .status(Status.WAITING).createdAt(NOW).build(); - given(reservationRepository.findById(2L)).willReturn(Optional.of(waiting)); - - reservationService.cancel(2L, LocalDateTime.MIN); - - verify(reservationRepository).deleteById(2L); - org.mockito.Mockito.verifyNoMoreInteractions(reservationRepository); + @DisplayName("승인 예약이 빈 슬롯으로 옮겨지면 첫 대기 예약이 승격된다") + void promote_first_waiting_when_approved_moved_out() { + // given + LocalDate targetDate = LocalDate.MAX; + Slot futureSlot = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, + RoomEscapeFixture.FUTURE_DATE); + Slot targetSlot = saveSlot(RoomEscapeFixture.theme(), RoomEscapeFixture.TIME, + new ReservationDate(targetDate)); + + String approvedName = "zeze"; + String targetSlotName = "dalsu"; + + Reservation approvedReservation = saveReservation(futureSlot, approvedName, Status.APPROVED); + Reservation targetSlotReservation = saveReservation(targetSlot, targetSlotName, Status.APPROVED); + + ReservationUpdateRequest request = new ReservationUpdateRequest(approvedName, targetDate, + targetSlot.getTime().getId(), + targetSlot.getTheme().getId()); + + // when + RankedReservation updated = reservationService.update(request, approvedReservation.getId(), + LocalDateTime.now()); + + // then + assertThat(updated.getRank().getValue()).isEqualTo(1); + assertThat(updated.getReservation().getStatus()).isEqualTo(Status.WAITING); } + } - @Test - void 존재하지_않는_예약_취소시_예외_발생() { - given(reservationRepository.findById(NOT_EXISTS_ID)).willReturn(Optional.empty()); + private Reservation saveReservation(Slot slot, String name, Status status) { + return reservationRepository.save( + RoomEscapeFixture.reservation().name(name).slot(slot).status(status).build()); + } - Assertions.assertThatThrownBy(() -> reservationService.cancel(NOT_EXISTS_ID, NOW)) - .isInstanceOf(RoomEscapeException.class); - } + private Slot saveSlot(Theme theme, ReservationTime time, ReservationDate date) { + Theme targetTheme = themeRepository.save(theme); + ReservationTime targetTime = reservationTimeRepository.save(time); + return slotRepository.save( + RoomEscapeFixture.slot().time(targetTime).theme(targetTheme).date(date).build()); } } From 3a052f2f3ec1da18ab82d5abefda1e9feee2950d Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 11 Jun 2026 14:54:28 +0900 Subject: [PATCH 18/26] =?UTF-8?q?refactor:=20Entity=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=20=EB=AF=B8=EC=98=81=EC=86=8D=20=EC=83=81=ED=83=9C=EC=9E=84?= =?UTF-8?q?=EC=9D=84=20=EB=82=98=ED=83=80=EB=82=B4=EB=8A=94=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/domain/reservation/Reservation.java | 3 ++- .../java/roomescape/domain/reservation/ReservationTime.java | 3 ++- src/main/java/roomescape/domain/reservation/Slot.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index eba9819a6c..2568a02d1a 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -7,6 +7,7 @@ import roomescape.domain.theme.Theme; public class Reservation { + private static final long TRANSIENT = 0L; private final long id; private final ReservationName name; private final Slot slot; @@ -28,7 +29,7 @@ public static Reservation load(long id, ReservationName reservationName, Slot sl public static Reservation create(ReservationName reservationName, Slot slot, Status status, LocalDateTime now) { Objects.requireNonNull(now); - Reservation reservation = new Reservation(0L, reservationName, slot, status, now); + Reservation reservation = new Reservation(TRANSIENT, reservationName, slot, status, now); reservation.isPastFrom(now); return reservation; } diff --git a/src/main/java/roomescape/domain/reservation/ReservationTime.java b/src/main/java/roomescape/domain/reservation/ReservationTime.java index 2f3e9041de..9f7e41e4fc 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservation/ReservationTime.java @@ -4,6 +4,7 @@ import java.util.Objects; public class ReservationTime { + private static final long TRANSIENT = 0L; private final long id; private final LocalTime startAt; @@ -17,7 +18,7 @@ public static ReservationTime of(long id, LocalTime startAt) { } public static ReservationTime of(LocalTime startAt) { - return new ReservationTime(0L, startAt); + return new ReservationTime(TRANSIENT, startAt); } public ReservationTime withId(long id) { diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index 7f63b43879..8a66e121b0 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -5,6 +5,7 @@ import roomescape.domain.theme.Theme; public class Slot { + private static final int TRANSIENT = 0; private final long id; private final ReservationDate date; private final ReservationTime time; @@ -22,7 +23,7 @@ public static Slot load(long id, ReservationDate date, ReservationTime time, The } public static Slot create(ReservationDate date, ReservationTime time, Theme theme) { - return new Slot(0, date, time, theme); + return new Slot(TRANSIENT, date, time, theme); } public Slot withId(long id) { From 488472737bf7cdb63d9b44b9ecef3e511533a13f Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 11 Jun 2026 14:57:38 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refactor:=20isPastFrom=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/domain/reservation/Reservation.java | 4 ++-- src/main/java/roomescape/service/ReservationService.java | 4 ++-- .../java/roomescape/domain/reservation/ReservationTest.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 2568a02d1a..f837683888 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -30,11 +30,11 @@ public static Reservation load(long id, ReservationName reservationName, Slot sl public static Reservation create(ReservationName reservationName, Slot slot, Status status, LocalDateTime now) { Objects.requireNonNull(now); Reservation reservation = new Reservation(TRANSIENT, reservationName, slot, status, now); - reservation.isPastFrom(now); + reservation.ensureNotPast(now); return reservation; } - public void isPastFrom(LocalDateTime now) { + public void ensureNotPast(LocalDateTime now) { if (slot.isBefore(now)) { throw new RoomEscapeException(ErrorCode.PAST_RESERVATION_NOT_ALLOWED); } diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 1b38eb5159..ba4fc8e04e 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -58,7 +58,7 @@ public List findList(String name) { @Transactional public RankedReservation update(ReservationUpdateRequest request, long id, LocalDateTime now) { Reservation originReservation = findReservationById(id); - originReservation.isPastFrom(now); + originReservation.ensureNotPast(now); Slot updateSlot = slotService.findOrCreate(request.getDate(), request.getTimeId(), request.getThemeId()); slotService.lockSlot(updateSlot); @@ -83,7 +83,7 @@ private void findFirstWaitingAndUpdateStatus(Reservation reservation) { @Transactional public void cancel(long reservationId, LocalDateTime now) { Reservation reservation = findReservationById(reservationId); - reservation.isPastFrom(now); + reservation.ensureNotPast(now); reservationRepository.deleteById(reservationId); diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 5420920d6a..29f909bc11 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -54,7 +54,7 @@ static Stream nullCases() { Slot slot = RoomEscapeFixture.slot().date(RoomEscapeFixture.PAST_DATE).build(); Reservation past = RoomEscapeFixture.reservation().slot(slot).build(); - Assertions.assertThatThrownBy(() -> past.isPastFrom(LocalDateTime.now())) + Assertions.assertThatThrownBy(() -> past.ensureNotPast(LocalDateTime.now())) .isInstanceOf(RoomEscapeException.class); } @@ -63,6 +63,6 @@ static Stream nullCases() { Slot slot = RoomEscapeFixture.slot().date(RoomEscapeFixture.FUTURE_DATE).build(); Reservation future = RoomEscapeFixture.reservation().slot(slot).build(); - Assertions.assertThatCode(() -> future.isPastFrom(LocalDateTime.now())); + Assertions.assertThatCode(() -> future.ensureNotPast(LocalDateTime.now())); } } From db935b691613cc5a351f4899accff5192c4ecd64 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 11 Jun 2026 15:03:48 +0900 Subject: [PATCH 20/26] =?UTF-8?q?refactor:=20entity=20equals=20&=20hashCod?= =?UTF-8?q?e=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/domain/reservation/Slot.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index 8a66e121b0..2d8fa73b76 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -55,18 +55,4 @@ public ReservationTime getTime() { public Theme getTheme() { return theme; } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - Slot slot = (Slot) o; - return id == slot.id; - } - - @Override - public int hashCode() { - return Objects.hashCode(id); - } } From aa73a4635549fcfedec0379b8fd6bba4e4dce605 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 11 Jun 2026 15:06:05 +0900 Subject: [PATCH 21/26] =?UTF-8?q?refactor:=20ENUM=20=EB=B9=84=EA=B5=90=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A7=81=EC=A0=91=20=EB=B9=84=EA=B5=90=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/domain/reservation/Reservation.java | 2 +- src/main/java/roomescape/domain/reservation/Status.java | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index f837683888..44c76f2fca 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -53,7 +53,7 @@ public boolean isSameSlot(Slot target) { } public boolean isApproved() { - return status.isApproved(); + return status == Status.APPROVED; } public Reservation withId(long id) { diff --git a/src/main/java/roomescape/domain/reservation/Status.java b/src/main/java/roomescape/domain/reservation/Status.java index da3a8b5b34..1df323f658 100644 --- a/src/main/java/roomescape/domain/reservation/Status.java +++ b/src/main/java/roomescape/domain/reservation/Status.java @@ -13,8 +13,4 @@ public enum Status { public String getKoreanName() { return koreanName; } - - public boolean isApproved() { - return this == APPROVED; - } } From db0de31144823db76da30d2fb941ff92fb76a099 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 11 Jun 2026 15:13:06 +0900 Subject: [PATCH 22/26] =?UTF-8?q?refactor:=20=EB=94=94=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=EC=9D=98=20=EB=B2=95=EC=B9=99=EC=9D=84=20=EC=A4=80=EC=88=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/Reservation.java | 12 ++++++++++++ .../domain/reservation/Reservations.java | 16 ++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 44c76f2fca..b56c4d1b38 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -48,6 +48,14 @@ public boolean isEarlierThan(Reservation target) { return id < target.getId(); } + public boolean hasSameSlot(Reservation target) { + return slot.isSame(target.slot); + } + + public boolean hasSameSlot(Slot slot) { + return slot.isSame(slot); + } + public boolean isSameSlot(Slot target) { return slot.isSame(target); } @@ -56,6 +64,10 @@ public boolean isApproved() { return status == Status.APPROVED; } + public boolean hasSameName(ReservationName name) { + return name.equals(name); + } + public Reservation withId(long id) { return new Reservation(id, name, slot, status, createdAt); } diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java index 80a3a6f8eb..a9299053e0 100644 --- a/src/main/java/roomescape/domain/reservation/Reservations.java +++ b/src/main/java/roomescape/domain/reservation/Reservations.java @@ -22,17 +22,21 @@ public Reservation reserve(ReservationName reservationName, Slot foundSlot, Loca } private void validateNoDuplicate(ReservationName reservationName, Slot slot) { - if (reservations.stream() - .filter(reservation -> reservation.getSlot().equals(slot)) - .anyMatch(reservation -> reservation.getName().equals(reservationName))) { + if (hasNameAndSlot(reservationName, slot)) { throw new RoomEscapeException(ErrorCode.DUPLICATE_RESERVATION); } } + private boolean hasNameAndSlot(ReservationName reservationName, Slot slot) { + return reservations.stream() + .filter(reservation -> reservation.hasSameSlot(slot)) + .anyMatch(reservation -> reservation.hasSameName(reservationName)); + } + private Status decideStatusFor(Slot slot) { if (reservations.stream() .filter(reservation -> reservation.isSameSlot(slot)) - .anyMatch(reservation -> reservation.getStatus().equals(Status.APPROVED))) { + .anyMatch(reservation -> reservation.isApproved())) { return Status.WAITING; } return Status.APPROVED; @@ -48,7 +52,7 @@ public List rankedReservationsOf(String name) { private List findByName(String name) { return reservations.stream() - .filter(reservation -> reservation.getName().equals(new ReservationName(name))) + .filter(reservation -> reservation.hasSameName(new ReservationName(name))) .toList(); } @@ -60,7 +64,7 @@ public List allRankedReservationsOf() { private RankedReservation toRankedReservation(Reservation target) { List sameSlots = reservations.stream() - .filter(reservation -> reservation.isSameSlot(target.getSlot())) + .filter(reservation -> reservation.hasSameSlot(target)) .toList(); return RankedReservation.decideRankFrom(target, sameSlots); From adc5a62374afe6ca1058d7a898e8a18bf117743a Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 11 Jun 2026 16:03:20 +0900 Subject: [PATCH 23/26] =?UTF-8?q?refactor:=20=EB=8F=99=EC=8B=9C=EC=84=B1?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/Reservation.java | 6 +- .../java/roomescape/RoomEscapeFixture.java | 24 ------ .../roomescape/RoomescapeApplicationTest.java | 56 +++++++++++++ .../domain/reservation/ReservationsTest.java | 1 - .../ReservationServiceIntegrationTest.java | 80 ------------------- 5 files changed, 59 insertions(+), 108 deletions(-) delete mode 100644 src/test/java/roomescape/service/ReservationServiceIntegrationTest.java diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index b56c4d1b38..dd554244d0 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -52,8 +52,8 @@ public boolean hasSameSlot(Reservation target) { return slot.isSame(target.slot); } - public boolean hasSameSlot(Slot slot) { - return slot.isSame(slot); + public boolean hasSameSlot(Slot target) { + return slot.isSame(target); } public boolean isSameSlot(Slot target) { @@ -65,7 +65,7 @@ public boolean isApproved() { } public boolean hasSameName(ReservationName name) { - return name.equals(name); + return this.name.equals(name); } public Reservation withId(long id) { diff --git a/src/test/java/roomescape/RoomEscapeFixture.java b/src/test/java/roomescape/RoomEscapeFixture.java index 1bd7c5145d..f4011e11f1 100644 --- a/src/test/java/roomescape/RoomEscapeFixture.java +++ b/src/test/java/roomescape/RoomEscapeFixture.java @@ -48,38 +48,14 @@ public static ThemeFamousFindRequest themeFamousFindRequest() { return new ThemeFamousFindRequest(7L, FUTURE_DATE.getValue(), 10L); } - public static ReservationCreateRequest reservationCreateRequest() { - return new ReservationCreateRequest(NAME.getValue(), FUTURE_DATE.getValue(), 1L, 1L); - } - public static ReservationCreateRequest reservationCreateRequestWithName(ReservationName name) { return new ReservationCreateRequest(name.getValue(), FUTURE_DATE.getValue(), 1L, 1L); } - public static ReservationCreateRequest reservationCreateRequestWithNullName() { - return new ReservationCreateRequest(null, FUTURE_DATE.getValue(), 1L, 1L); - } - - public static ReservationCreateRequest reservationCreateRequestWithNullDate() { - return new ReservationCreateRequest(NAME.getValue(), null, 1L, 1L); - } - - public static ReservationCreateRequest reservationCreateRequestWithNullTimeId() { - return new ReservationCreateRequest(NAME.getValue(), FUTURE_DATE.getValue(), null, 1L); - } - - public static ReservationCreateRequest reservationCreateRequestWithPastDate() { - return new ReservationCreateRequest(NAME.getValue(), PAST_DATE.getValue(), 1L, 1L); - } - public static ReservationUpdateRequest reservationUpdateRequest() { return new ReservationUpdateRequest(NAME.getValue(), FUTURE_DATE.getValue(), 1L, 1L); } - public static ReservationUpdateRequest reservationUpdateRequestWithPastDate() { - return new ReservationUpdateRequest(NAME.getValue(), PAST_DATE.getValue(), 1L, 1L); - } - public static class SlotBuilder { private long id = 1L; private ReservationDate date = FUTURE_DATE; diff --git a/src/test/java/roomescape/RoomescapeApplicationTest.java b/src/test/java/roomescape/RoomescapeApplicationTest.java index 9b66e38c5a..951011a32e 100644 --- a/src/test/java/roomescape/RoomescapeApplicationTest.java +++ b/src/test/java/roomescape/RoomescapeApplicationTest.java @@ -5,8 +5,13 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,6 +20,11 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; +import roomescape.controller.dto.request.ReservationCreateRequest; +import roomescape.domain.reservation.RankedReservation; +import roomescape.domain.reservation.ReservationName; +import roomescape.domain.reservation.Status; +import roomescape.service.ReservationService; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @@ -23,6 +33,9 @@ class RoomescapeApplicationTest { @Autowired private JdbcTemplate jdbcTemplate; + @Autowired + private ReservationService reservationService; + @LocalServerPort int port; @@ -270,6 +283,49 @@ private int availableCount(String date, long themeId) { reserve("zeze", "2099-06-01", 1L, 999L, 404); } + + @Test + void 동시에_10명이_첫_예약_요청시_1명만_승인상태가_된다() throws Exception { + // 한 슬롯에 Approve된 예약은 반드시 1건 이하여야 한다. + int threads = 10; + var ready = new CountDownLatch(threads); + var start = new CountDownLatch(1); + var done = new CountDownLatch(threads); + var approved = new AtomicInteger(); + var waiting = new AtomicInteger(); + + var pool = Executors.newFixedThreadPool(threads); + + for (int i = 0; i < threads; i++) { + ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequestWithName( + new ReservationName(i + "")); + pool.submit(() -> { + ready.countDown(); + try { + start.await(); + RankedReservation result = reservationService.reserve(request, LocalDateTime.now()); + + if (result.getReservation().getStatus() == Status.APPROVED) { + approved.incrementAndGet(); + } + if (result.getReservation().getStatus() == Status.WAITING) { + waiting.incrementAndGet(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + done.countDown(); + } + }); + } + ready.await(); + start.countDown(); + done.await(); + + AssertionsForClassTypes.assertThat(approved.get()).isEqualTo(1); + AssertionsForClassTypes.assertThat(waiting.get()).isEqualTo(9); + } + private int reserveAndGetId(String name, String date, Long timeId, Long themeId) { Map params = new HashMap<>(); params.put("name", name); diff --git a/src/test/java/roomescape/domain/reservation/ReservationsTest.java b/src/test/java/roomescape/domain/reservation/ReservationsTest.java index d4a8bf47fa..73bae2957e 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationsTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationsTest.java @@ -69,7 +69,6 @@ class ReservationsTest { assertThat(result.getName()).isEqualTo(NAME); } - @Test void 같은_슬롯에서_먼저_예약한_사람이_rank_0이다() { Reservation first = RoomEscapeFixture.reservation() diff --git a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java b/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java deleted file mode 100644 index 499a2e8e0f..0000000000 --- a/src/test/java/roomescape/service/ReservationServiceIntegrationTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package roomescape.service; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -import java.time.LocalDateTime; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.annotation.DirtiesContext; -import roomescape.RoomEscapeFixture; -import roomescape.controller.dto.request.ReservationCreateRequest; -import roomescape.domain.reservation.RankedReservation; -import roomescape.domain.reservation.ReservationName; -import roomescape.domain.reservation.Status; - -@SpringBootTest(webEnvironment = WebEnvironment.NONE) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -class ReservationServiceIntegrationTest { - @Autowired - private ReservationService reservationService; - - @Autowired - private JdbcTemplate jdbcTemplate; - - @BeforeEach - 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')"); - } - - @Test - void 동시에_10명이_첫_예약_요청시_1명만_승인상태가_된다() throws Exception { - // 한 슬롯에 Approve된 예약은 반드시 1건 이하여야 한다. - int threads = 10; - var ready = new CountDownLatch(threads); - var start = new CountDownLatch(1); - var done = new CountDownLatch(threads); - var approved = new AtomicInteger(); - var waiting = new AtomicInteger(); - - var pool = Executors.newFixedThreadPool(threads); - - for (int i = 0; i < threads; i++) { - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequestWithName( - new ReservationName(i + "")); - pool.submit(() -> { - ready.countDown(); - try { - start.await(); - RankedReservation result = reservationService.reserve(request, LocalDateTime.now()); - - if (result.getReservation().getStatus() == Status.APPROVED) { - approved.incrementAndGet(); - } - if (result.getReservation().getStatus() == Status.WAITING) { - waiting.incrementAndGet(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - done.countDown(); - } - }); - } - ready.await(); - start.countDown(); - done.await(); - - assertThat(approved.get()).isEqualTo(1); - assertThat(waiting.get()).isEqualTo(9); - } -} - From 43fbde4294d9d47b1d9032a57f2ce52ea00ad8e5 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Tue, 16 Jun 2026 12:53:11 +0900 Subject: [PATCH 24/26] =?UTF-8?q?[1=20=EB=8B=A8=EA=B3=84]=20JPA=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../domain/reservation/ReservationTime.java | 17 +- .../java/roomescape/domain/theme/Theme.java | 27 +- .../roomescape/domain/theme/ThemeName.java | 9 +- .../roomescape/domain/theme/ThumbnailUrl.java | 9 +- .../ReservationTimeJpaRepository.java | 9 + .../repository/ReservationTimeRepository.java | 44 -- .../repository/ThemeJpaRepository.java | 9 + .../repository/ThemeRepository.java | 45 -- .../service/ReservationTimeService.java | 12 +- .../java/roomescape/service/SlotService.java | 17 +- .../java/roomescape/service/ThemeService.java | 17 +- src/main/resources/application.properties | 8 +- .../repository/ReservationRepositoryTest.java | 425 ------------------ .../repository/SlotRepositoryTest.java | 117 ----- .../service/ReservationServiceTest.java | 8 +- .../service/ReservationTimeServiceTest.java | 4 +- .../roomescape/service/ThemeServiceTest.java | 9 +- 18 files changed, 115 insertions(+), 672 deletions(-) create mode 100644 src/main/java/roomescape/repository/ReservationTimeJpaRepository.java create mode 100644 src/main/java/roomescape/repository/ThemeJpaRepository.java delete mode 100644 src/test/java/roomescape/repository/ReservationRepositoryTest.java delete mode 100644 src/test/java/roomescape/repository/SlotRepositoryTest.java diff --git a/build.gradle b/build.gradle index 4d675e7b13..59b5993f0b 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' diff --git a/src/main/java/roomescape/domain/reservation/ReservationTime.java b/src/main/java/roomescape/domain/reservation/ReservationTime.java index 9f7e41e4fc..8b5f5e2873 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservation/ReservationTime.java @@ -1,12 +1,25 @@ 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 java.util.Objects; +@Entity public class ReservationTime { private static final long TRANSIENT = 0L; - 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; diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index 5c8c99f58e..5aaec61523 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -1,14 +1,29 @@ 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; +@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; - private Theme(long id, ThemeName name, String description, ThumbnailUrl thumbnailUrl) { + protected Theme() { + } + + private Theme(Long id, ThemeName name, String description, ThumbnailUrl thumbnailUrl) { this.id = id; this.name = Objects.requireNonNull(name); this.description = Objects.requireNonNull(description); @@ -20,7 +35,7 @@ public static Theme load(long id, ThemeName name, String description, ThumbnailU } public static Theme create(ThemeName name, String description, ThumbnailUrl thumbnailUrl) { - return new Theme(0L, name, description, thumbnailUrl); + return new Theme(null, name, description, thumbnailUrl); } public Theme withId(long id) { diff --git a/src/main/java/roomescape/domain/theme/ThemeName.java b/src/main/java/roomescape/domain/theme/ThemeName.java index 7eab51f1e2..9632887ea6 100644 --- a/src/main/java/roomescape/domain/theme/ThemeName.java +++ b/src/main/java/roomescape/domain/theme/ThemeName.java @@ -2,13 +2,20 @@ import common.exception.ErrorCode; import common.exception.RoomEscapeException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; import java.util.Objects; +@Embeddable public class ThemeName { private static final int MIN_NAME_LENGTH = 1; private static final int MAX_NAME_LENGTH = 30; - private final String value; + @Column(name = "name", nullable = false, length = 20) + private String value; + + protected ThemeName() { + } public ThemeName(String value) { Objects.requireNonNull(value); diff --git a/src/main/java/roomescape/domain/theme/ThumbnailUrl.java b/src/main/java/roomescape/domain/theme/ThumbnailUrl.java index 83e4729e16..3bb49cd69b 100644 --- a/src/main/java/roomescape/domain/theme/ThumbnailUrl.java +++ b/src/main/java/roomescape/domain/theme/ThumbnailUrl.java @@ -2,13 +2,20 @@ import common.exception.ErrorCode; import common.exception.RoomEscapeException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; import java.util.Objects; import java.util.regex.Pattern; +@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) { validate(value); diff --git a/src/main/java/roomescape/repository/ReservationTimeJpaRepository.java b/src/main/java/roomescape/repository/ReservationTimeJpaRepository.java new file mode 100644 index 0000000000..59697926e8 --- /dev/null +++ b/src/main/java/roomescape/repository/ReservationTimeJpaRepository.java @@ -0,0 +1,9 @@ +package roomescape.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import roomescape.domain.reservation.ReservationTime; + +@Repository +public interface ReservationTimeJpaRepository extends JpaRepository { +} diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java index 1b8b6a4e9b..817029f6d5 100644 --- a/src/main/java/roomescape/repository/ReservationTimeRepository.java +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -2,11 +2,8 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; -import java.util.Optional; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; import roomescape.domain.reservation.ReservationTime; @@ -14,42 +11,11 @@ public class ReservationTimeRepository { private static final RowMapper RESERVATION_TIME_ROW_MAPPER = (rs, rowNum) -> RepositoryRowMapper.reservationTimeRowMapper(rs); - private static final String EXISTS_BY_ID = """ - SELECT EXISTS ( - SELECT 1 - FROM reservation_time - WHERE id = ? - ) - """; private final JdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert simpleJdbcInsert; public ReservationTimeRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; - this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) - .withTableName("reservation_time") - .usingGeneratedKeyColumns("id"); - } - - public ReservationTime save(ReservationTime time) { - Map params = Map.of( - "start_at", time.getStartAt()); - - long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - - return time.withId(generatedKey); - } - - public List findAll() { - String sql = "select id AS time_id, start_at from reservation_time"; - return jdbcTemplate.query(sql, RESERVATION_TIME_ROW_MAPPER); - } - - public Optional findById(long id) { - String sql = "select id AS time_id, start_at from reservation_time where id = ?"; - List result = jdbcTemplate.query(sql, RESERVATION_TIME_ROW_MAPPER, id); - return result.stream().findFirst(); } public List findByDateAndTheme(LocalDate date, long themeId) { @@ -65,14 +31,4 @@ WHERE rt.id NOT IN ( """; return jdbcTemplate.query(sql, RESERVATION_TIME_ROW_MAPPER, date, themeId); } - - public void delete(long id) { - String sql = "delete from reservation_time where id = ?"; - jdbcTemplate.update(sql, id); - } - - public boolean existsById(long reservationTimeId) { - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(EXISTS_BY_ID, Boolean.class, reservationTimeId)); - } } diff --git a/src/main/java/roomescape/repository/ThemeJpaRepository.java b/src/main/java/roomescape/repository/ThemeJpaRepository.java new file mode 100644 index 0000000000..12470f79cb --- /dev/null +++ b/src/main/java/roomescape/repository/ThemeJpaRepository.java @@ -0,0 +1,9 @@ +package roomescape.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import roomescape.domain.theme.Theme; + +@Repository +public interface ThemeJpaRepository extends JpaRepository { +} diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java index ce777264ce..55a03c9d84 100644 --- a/src/main/java/roomescape/repository/ThemeRepository.java +++ b/src/main/java/roomescape/repository/ThemeRepository.java @@ -1,11 +1,8 @@ package roomescape.repository; import java.util.List; -import java.util.Map; -import java.util.Optional; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; import roomescape.domain.theme.FamousThemeCondition; import roomescape.domain.theme.Theme; @@ -13,37 +10,11 @@ @Repository public class ThemeRepository { public static final RowMapper THEME_ROW_MAPPER = (rs, rowNum) -> RepositoryRowMapper.themeRowMapper(rs); - private static final String EXISTS_BY_ID = """ - SELECT EXISTS ( - SELECT 1 - FROM theme - WHERE id = ? - ) - """; private final JdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert simpleJdbcInsert; public ThemeRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; - this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) - .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() { - String sql = "SELECT id AS theme_id, name AS theme_name, description, thumbnail_url FROM THEME"; - return jdbcTemplate.query(sql, THEME_ROW_MAPPER); } public List findFamous(FamousThemeCondition condition) { @@ -65,20 +36,4 @@ ORDER BY count(s.theme_id) DESC, s.theme_id DESC return jdbcTemplate.query(sql, THEME_ROW_MAPPER, condition.startDate(), condition.endDate(), condition.getLimit()); } - - public void deleteById(long themeId) { - String sql = "DELETE FROM theme WHERE id = ?"; - jdbcTemplate.update(sql, themeId); - } - - public boolean existsById(long themeId) { - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(EXISTS_BY_ID, Boolean.class, themeId)); - } - - public Optional findById(long themeId) { - String sql = "SELECT id AS theme_id, name AS theme_name, description, thumbnail_url FROM THEME WHERE id = ?"; - List result = jdbcTemplate.query(sql, THEME_ROW_MAPPER, themeId); - return result.stream().findFirst(); - } } diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java index c741ebfe44..eec798ad00 100644 --- a/src/main/java/roomescape/service/ReservationTimeService.java +++ b/src/main/java/roomescape/service/ReservationTimeService.java @@ -10,28 +10,32 @@ import roomescape.controller.dto.request.ReservationTimeCreateRequest; import roomescape.domain.reservation.ReservationTime; import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationTimeJpaRepository; import roomescape.repository.ReservationTimeRepository; @Service @Transactional(readOnly = true) public class ReservationTimeService { private final ReservationTimeRepository reservationTimeRepository; + private final ReservationTimeJpaRepository reservationTimeJpaRepository; private final ReservationRepository reservationRepository; public ReservationTimeService(ReservationTimeRepository reservationTimeRepository, + ReservationTimeJpaRepository reservationTimeJpaRepository, ReservationRepository reservationRepository) { this.reservationTimeRepository = reservationTimeRepository; + this.reservationTimeJpaRepository = reservationTimeJpaRepository; this.reservationRepository = reservationRepository; } @Transactional public ReservationTime create(ReservationTimeCreateRequest request) { ReservationTime reservationTime = ReservationTime.of(request.getStartAt()); - return reservationTimeRepository.save(reservationTime); + return reservationTimeJpaRepository.save(reservationTime); } public List findAll() { - return reservationTimeRepository.findAll(); + return reservationTimeJpaRepository.findAll(); } public List findAvailable(AvailableTimeFindRequest request, LocalDate now) { @@ -44,7 +48,7 @@ public List findAvailable(AvailableTimeFindRequest request, Loc @Transactional public void delete(long reservationTimeId) { - if (!reservationTimeRepository.existsById(reservationTimeId)) { + if (!reservationTimeJpaRepository.existsById(reservationTimeId)) { throw new RoomEscapeException(ErrorCode.RESERVATION_TIME_NOT_FOUND); } @@ -52,6 +56,6 @@ public void delete(long reservationTimeId) { throw new RoomEscapeException(ErrorCode.RESERVATION_TIME_IN_USE); } - reservationTimeRepository.delete(reservationTimeId); + reservationTimeJpaRepository.deleteById(reservationTimeId); } } diff --git a/src/main/java/roomescape/service/SlotService.java b/src/main/java/roomescape/service/SlotService.java index 9ec8ed89db..1a05363e12 100644 --- a/src/main/java/roomescape/service/SlotService.java +++ b/src/main/java/roomescape/service/SlotService.java @@ -10,19 +10,19 @@ import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.theme.Theme; -import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.ReservationTimeJpaRepository; import roomescape.repository.SlotRepository; -import roomescape.repository.ThemeRepository; +import roomescape.repository.ThemeJpaRepository; @Service @Transactional(readOnly = true) public class SlotService { private final SlotRepository slotRepository; - private final ReservationTimeRepository reservationTimeRepository; - private final ThemeRepository themeRepository; + private final ReservationTimeJpaRepository reservationTimeRepository; + private final ThemeJpaRepository themeRepository; - public SlotService(SlotRepository slotRepository, ReservationTimeRepository reservationTimeRepository, - ThemeRepository themeRepository) { + public SlotService(SlotRepository slotRepository, ReservationTimeJpaRepository reservationTimeRepository, + ThemeJpaRepository themeRepository) { this.slotRepository = slotRepository; this.reservationTimeRepository = reservationTimeRepository; this.themeRepository = themeRepository; @@ -48,11 +48,6 @@ private Slot saveOrReread(LocalDate date, ReservationTime time, Theme theme) { } } - public Slot findById(long slotId) { - return slotRepository.findById(slotId) - .orElseThrow(() -> new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND)); - } - public void lockSlot(Slot foundSlot) { if (!slotRepository.lockSlot(foundSlot)) { throw new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND); diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java index 259ca5fa57..faa7e58e2d 100644 --- a/src/main/java/roomescape/service/ThemeService.java +++ b/src/main/java/roomescape/service/ThemeService.java @@ -13,15 +13,19 @@ import roomescape.domain.theme.ThemeName; import roomescape.domain.theme.ThumbnailUrl; import roomescape.repository.ReservationRepository; +import roomescape.repository.ThemeJpaRepository; import roomescape.repository.ThemeRepository; @Service @Transactional(readOnly = true) public class ThemeService { + private final ThemeJpaRepository themeJpaRepository; private final ThemeRepository themeRepository; private final ReservationRepository reservationRepository; - public ThemeService(ThemeRepository themeRepository, ReservationRepository reservationRepository) { + public ThemeService(ThemeJpaRepository themeJpaRepository, ThemeRepository themeRepository, + ReservationRepository reservationRepository) { + this.themeJpaRepository = themeJpaRepository; this.themeRepository = themeRepository; this.reservationRepository = reservationRepository; } @@ -30,15 +34,16 @@ public ThemeService(ThemeRepository themeRepository, ReservationRepository reser public Theme create(ThemeCreateRequest request) { Theme theme = Theme.create(new ThemeName(request.getName()), request.getDescription(), new ThumbnailUrl(request.getThumbnailUrl())); - return themeRepository.save(theme); + return themeJpaRepository.save(theme); } public Theme find(long themeId) { - return themeRepository.findById(themeId).orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); + return themeJpaRepository.findById(themeId) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); } public List findAll() { - return themeRepository.findAll(); + return themeJpaRepository.findAll(); } public List findFamous(ThemeFamousFindRequest request, LocalDate now) { @@ -50,7 +55,7 @@ public List findFamous(ThemeFamousFindRequest request, LocalDate now) { @Transactional public void delete(long themeId) { - if (!themeRepository.existsById(themeId)) { + if (!themeJpaRepository.existsById(themeId)) { throw new RoomEscapeException(ErrorCode.THEME_NOT_FOUND); } @@ -58,6 +63,6 @@ public void delete(long themeId) { throw new RoomEscapeException(ErrorCode.THEME_IN_USE); } - themeRepository.deleteById(themeId); + themeJpaRepository.deleteById(themeId); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f597afeef4..b2bce96e46 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,9 @@ +# DB 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 +# JPA +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.ddl-auto=create-drop +spring.jpa.defer-datasource-initialization=true \ No newline at end of file diff --git a/src/test/java/roomescape/repository/ReservationRepositoryTest.java b/src/test/java/roomescape/repository/ReservationRepositoryTest.java deleted file mode 100644 index d5ac11594f..0000000000 --- a/src/test/java/roomescape/repository/ReservationRepositoryTest.java +++ /dev/null @@ -1,425 +0,0 @@ -package roomescape.repository; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -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 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; -import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeName; -import roomescape.domain.theme.ThumbnailUrl; - - -@JdbcTest -@Import(value = { - ReservationRepository.class, - ReservationTimeRepository.class, - ThemeRepository.class, - SlotRepository.class -}) -class ReservationRepositoryTest { - private static final LocalDate TODAY = LocalDate.of(2026, 5, 10); - private static final LocalDate FUTURE = LocalDate.of(2099, 1, 1); - - @Autowired - private ReservationRepository reservationRepository; - - @Autowired - private ReservationTimeRepository timeRepository; - - @Autowired - private ThemeRepository themeRepository; - - @Autowired - private SlotRepository slotRepository; - - private ReservationTime saveTimeAndGet(int hour) { - return timeRepository.save(ReservationTime.of(LocalTime.of(hour, 0))); - } - - private Theme saveThemeAndGet(String name) { - return themeRepository.save( - Theme.create(new ThemeName(name), name + " 설명", new ThumbnailUrl("https://test-theme.com"))); - } - - private Theme saveThemeAndGet() { - return saveThemeAndGet("테마"); - } - - private Slot getSlotOrCreate(LocalDate date, ReservationTime time, Theme theme) { - return slotRepository.findByDateAndTimeAndTheme(date, time, theme) - .orElseGet(() -> slotRepository.save(Slot.create(new ReservationDate(date), time, theme))); - } - - private Reservation reservation(String name, LocalDate date, ReservationTime time, Theme theme) { - return reservation(name, date, time, theme, Status.APPROVED); - } - - private Reservation reservation(String name, LocalDate date, ReservationTime time, Theme theme, Status status) { - Slot slot = getSlotOrCreate(date, time, theme); - return Reservation.load(0L, new ReservationName(name), slot, status, LocalDateTime.now()); - } - - @Nested - @DisplayName("save") - class Save { - - @Test - void 예약을_저장하면_ID가_부여된_예약이_반환된다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - Reservation saved = reservationRepository.save(reservation("제제", FUTURE, time, theme)); - - assertSoftly(soft -> { - soft.assertThat(saved.getId()).isPositive(); - soft.assertThat(saved.getName().getValue()).isEqualTo("제제"); - soft.assertThat(saved.getDate().getValue()).isEqualTo(FUTURE); - }); - } - - @Test - void 여러_예약을_저장하면_각기_다른_ID가_부여된다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - Reservation first = reservationRepository.save(reservation("달수", FUTURE, time, theme)); - Reservation second = reservationRepository.save(reservation("민구", FUTURE.plusDays(1), time, theme)); - - assertThat(first.getId()).isNotEqualTo(second.getId()); - } - } - - @Nested - @DisplayName("findAll") - class FindAll { - - @Test - void 예약이_없으면_빈_목록을_반환한다() { - assertThat(reservationRepository.findAll()).isEmpty(); - } - - @Test - void 저장된_예약을_모두_반환한다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - reservationRepository.save(reservation("달수", FUTURE, time, theme)); - reservationRepository.save(reservation("민구", FUTURE.plusDays(1), time, theme)); - - assertThat(reservationRepository.findAll()).hasSize(2); - } - } - - @Nested - @DisplayName("findAll (name filter)") - class FindAllByName { - - @Test - void 이름으로_조회하면_해당_이름의_예약만_반환된다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - reservationRepository.save(reservation("달수", FUTURE, time, theme)); - reservationRepository.save(reservation("달수", FUTURE.plusDays(1), time, theme)); - reservationRepository.save(reservation("민구", FUTURE.plusDays(2), time, theme)); - - assertThat(reservationRepository.findAll().stream() - .filter(r -> r.getName().getValue().equals("달수")) - .toList()).hasSize(2); - } - - @Test - void 존재하지_않는_이름으로_조회하면_빈_목록을_반환한다() { - assertThat(reservationRepository.findAll().stream() - .filter(r -> r.getName().getValue().equals("없는이름")) - .toList()).isEmpty(); - } - } - - @Nested - @DisplayName("findById") - class FindById { - - @Test - void ID로_조회하면_해당_예약이_반환된다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme)); - - assertThat(reservationRepository.findById(saved.getId())).isPresent(); - } - - @Test - void 존재하지_않는_ID로_조회하면_빈_Optional을_반환한다() { - assertThat(reservationRepository.findById(Long.MAX_VALUE)).isEmpty(); - } - - @Test - void 조회한_예약의_필드가_저장된_값과_일치한다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme)); - Reservation found = reservationRepository.findById(saved.getId()).orElseThrow(); - - assertSoftly(soft -> { - soft.assertThat(found.getName().getValue()).isEqualTo("달수"); - soft.assertThat(found.getDate().getValue()).isEqualTo(FUTURE); - soft.assertThat(found.getTime().getId()).isEqualTo(time.getId()); - soft.assertThat(found.getTheme().getId()).isEqualTo(theme.getId()); - }); - } - } - - @Nested - @DisplayName("update") - class Update { - - @Test - void 예약을_수정하면_변경된_내용이_반영된다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time1 = saveTimeAndGet(10); - ReservationTime time2 = saveTimeAndGet(14); - - Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time1, theme)); - Reservation target = reservation("민구", FUTURE.plusDays(1), time2, theme); - - Reservation updated = reservationRepository.update(saved.getId(), target); - - assertSoftly(soft -> { - soft.assertThat(updated.getId()).isEqualTo(saved.getId()); - soft.assertThat(updated.getName().getValue()).isEqualTo("민구"); - soft.assertThat(updated.getDate().getValue()).isEqualTo(FUTURE.plusDays(1)); - soft.assertThat(updated.getTime().getId()).isEqualTo(time2.getId()); - }); - } - } - - @Nested - @DisplayName("deleteById") - class DeleteById { - - @Test - void 예약을_삭제하면_조회할_수_없다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme)); - reservationRepository.deleteById(saved.getId()); - - assertThat(reservationRepository.findById(saved.getId())).isEmpty(); - } - - @Test - void 예약을_삭제하면_전체_목록에서도_제외된다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - Reservation r1 = reservationRepository.save(reservation("달수", FUTURE, time, theme)); - reservationRepository.save(reservation("민구", FUTURE.plusDays(1), time, theme)); - - reservationRepository.deleteById(r1.getId()); - - assertThat(reservationRepository.findAll()).hasSize(1); - } - } - - @Nested - @DisplayName("findAllBySlot") - class FindAllBySlot { - - @Test - void 같은_슬롯의_예약을_모두_반환한다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - reservationRepository.save(reservation("달수", FUTURE, time, theme)); - reservationRepository.save(reservation("민구", FUTURE, time, theme)); - - Slot slot = getSlotOrCreate(FUTURE, time, theme); - assertThat(reservationRepository.findAllBySlot(slot)).hasSize(2); - } - - @Test - void 다른_날짜의_예약은_포함되지_않는다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - reservationRepository.save(reservation("달수", FUTURE, time, theme)); - reservationRepository.save(reservation("민구", FUTURE.plusDays(1), time, theme)); - - Slot slot = getSlotOrCreate(FUTURE, time, theme); - assertThat(reservationRepository.findAllBySlot(slot)).hasSize(1); - } - } - - @Nested - @DisplayName("existsByTimeId / existsByThemeId") - class ExistsByFk { - - @Test - void 해당_시간으로_예약이_있으면_true를_반환한다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - reservationRepository.save(reservation("달수", FUTURE, time, theme)); - - assertThat(reservationRepository.existsByTimeId(time.getId())).isTrue(); - } - - @Test - void 해당_시간으로_예약이_없으면_false를_반환한다() { - ReservationTime time = saveTimeAndGet(10); - - assertThat(reservationRepository.existsByTimeId(time.getId())).isFalse(); - } - - @Test - void 해당_테마로_예약이_있으면_true를_반환한다() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(10); - - reservationRepository.save(reservation("달수", FUTURE, time, theme)); - - assertThat(reservationRepository.existsByThemeId(theme.getId())).isTrue(); - } - - @Test - void 해당_테마로_예약이_없으면_false를_반환한다() { - Theme theme = saveThemeAndGet("테마1"); - - assertThat(reservationRepository.existsByThemeId(theme.getId())).isFalse(); - } - } - - @Nested - @DisplayName("existsBySlotAndName") - class Exists { - - @Test - void 같은_슬롯과_이름이면_true() { - Theme theme = saveThemeAndGet("테마1"); - ReservationTime time = saveTimeAndGet(14); - - String name = "달수"; - reservationRepository.save(reservation(name, TODAY, time, theme)); - - Slot slot = getSlotOrCreate(TODAY, time, theme); - assertThat(reservationRepository.existsBySlotAndName(slot, name)).isTrue(); - } - - @Test - void 슬롯이나_이름이_다르면_false() { - Theme theme1 = saveThemeAndGet("테마1"); - Theme theme2 = saveThemeAndGet("테마2"); - ReservationTime time1 = saveTimeAndGet(14); - - String name = "달수"; - reservationRepository.save(reservation(name, TODAY, time1, theme1)); - - Slot slot1 = getSlotOrCreate(TODAY, time1, theme1); - - assertSoftly(soft -> { - soft.assertThat(reservationRepository.existsBySlotAndName(slot1, "other")).isFalse(); - Slot slot2 = getSlotOrCreate(TODAY, time1, theme2); - soft.assertThat(reservationRepository.existsBySlotAndName(slot2, name)).isFalse(); - }); - } - } - - @Nested - @DisplayName("existsApproved (slot 기준)") - class ExistsApproved { - - @Test - void APPROVED_예약이_있으면_true() { - Theme theme = saveThemeAndGet(); - ReservationTime time = saveTimeAndGet(14); - - reservationRepository.save(reservation("달수", TODAY, time, theme, Status.APPROVED)); - - Slot slot = getSlotOrCreate(TODAY, time, theme); - assertThat(reservationRepository.findAllBySlot(slot).stream() - .anyMatch(r -> r.getStatus() == Status.APPROVED)).isTrue(); - } - - @Test - void WAITING_예약만_있으면_false() { - Theme theme = saveThemeAndGet(); - ReservationTime time = saveTimeAndGet(14); - - reservationRepository.save(reservation("달수", TODAY, time, theme, Status.WAITING)); - - Slot slot = getSlotOrCreate(TODAY, time, theme); - assertThat(reservationRepository.findAllBySlot(slot).stream() - .anyMatch(r -> r.getStatus() == Status.APPROVED)).isFalse(); - } - } - - @Nested - @DisplayName("findFirstWaitingBySlot") - class FindFirstWaiting { - - @Test - void WAITING_예약이_있으면_가장_먼저_생성된_예약을_반환한다() { - Theme theme = saveThemeAndGet(); - ReservationTime time = saveTimeAndGet(14); - - reservationRepository.save(reservation("달수", TODAY, time, theme, Status.APPROVED)); - Reservation first = reservationRepository.save(reservation("민구", TODAY, time, theme, Status.WAITING)); - reservationRepository.save(reservation("철수", TODAY, time, theme, Status.WAITING)); - - Slot slot = getSlotOrCreate(TODAY, time, theme); - assertThat(reservationRepository.findFirstWaitingBySlot(slot)) - .isPresent() - .get() - .extracting(Reservation::getId) - .isEqualTo(first.getId()); - } - - @Test - void WAITING_예약이_없으면_빈_Optional을_반환한다() { - Theme theme = saveThemeAndGet(); - ReservationTime time = saveTimeAndGet(14); - - reservationRepository.save(reservation("달수", TODAY, time, theme, Status.APPROVED)); - - Slot slot = getSlotOrCreate(TODAY, time, theme); - assertThat(reservationRepository.findFirstWaitingBySlot(slot)).isEmpty(); - } - } - - @Nested - @DisplayName("updateStatus") - class UpdateStatus { - - @Test - void WAITING_예약을_APPROVED로_변경할_수_있다() { - Theme theme = saveThemeAndGet(); - ReservationTime time = saveTimeAndGet(14); - Reservation saved = reservationRepository.save(reservation("달수", FUTURE, time, theme, Status.WAITING)); - reservationRepository.updateStatus(saved.getId(), Status.APPROVED); - - assertThat(reservationRepository.findById(saved.getId())) - .isPresent() - .get() - .extracting(Reservation::getStatus) - .isEqualTo(Status.APPROVED); - } - } -} diff --git a/src/test/java/roomescape/repository/SlotRepositoryTest.java b/src/test/java/roomescape/repository/SlotRepositoryTest.java deleted file mode 100644 index b31076bc43..0000000000 --- a/src/test/java/roomescape/repository/SlotRepositoryTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package roomescape.repository; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import java.time.LocalDate; -import java.time.LocalTime; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -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 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.ThumbnailUrl; - -@JdbcTest -@Import(value = {SlotRepository.class, ReservationTimeRepository.class, ThemeRepository.class}) -class SlotRepositoryTest { - private static final LocalDate FUTURE = LocalDate.of(2099, 1, 1); - - @Autowired - private SlotRepository slotRepository; - - @Autowired - private ReservationTimeRepository timeRepository; - - @Autowired - private ThemeRepository themeRepository; - - private ReservationTime giveTime(int hour) { - return timeRepository.save(ReservationTime.of(LocalTime.of(hour, 0))); - } - - private Theme giveTheme(String name) { - return themeRepository.save( - Theme.create(new ThemeName(name), name + " 설명", new ThumbnailUrl("https://test-theme.com"))); - } - - @Nested - @DisplayName("save") - class Save { - - @Test - void 슬롯을_저장하면_ID가_부여된_슬롯이_반환된다() { - ReservationTime time = giveTime(10); - Theme theme = giveTheme("테마1"); - - Slot saved = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); - - assertSoftly(soft -> { - soft.assertThat(saved.getId()).isPositive(); - soft.assertThat(saved.getDate().getValue()).isEqualTo(FUTURE); - soft.assertThat(saved.getTime().getId()).isEqualTo(time.getId()); - soft.assertThat(saved.getTheme().getId()).isEqualTo(theme.getId()); - }); - } - } - - @Nested - @DisplayName("findById") - class FindById { - - @Test - void ID로_조회하면_슬롯이_반환된다() { - ReservationTime time = giveTime(10); - Theme theme = giveTheme("테마1"); - - Slot saved = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); - - assertThat(slotRepository.findById(saved.getId())).isPresent(); - } - - @Test - void 존재하지_않는_ID는_빈_Optional을_반환한다() { - assertThat(slotRepository.findById(Long.MAX_VALUE)).isEmpty(); - } - } - - @Nested - @DisplayName("findByDateAndTimeAndTheme") - class FindByDateAndTimeAndTheme { - - @Test - void 날짜_시간_테마가_일치하는_슬롯을_반환한다() { - ReservationTime time = giveTime(10); - Theme theme = giveTheme("테마1"); - - Slot saved = slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); - - assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE, time, theme)) - .isPresent() - .get() - .extracting(Slot::getId) - .isEqualTo(saved.getId()); - } - - @Test - void 조건이_하나라도_다르면_빈_Optional을_반환한다() { - ReservationTime time = giveTime(10); - ReservationTime otherTime = giveTime(11); - Theme theme = giveTheme("테마1"); - Theme otherTheme = giveTheme("테마2"); - slotRepository.save(Slot.create(new ReservationDate(FUTURE), time, theme)); - - assertSoftly(soft -> { - soft.assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE.plusDays(1), time, theme)).isEmpty(); - soft.assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE, otherTime, theme)).isEmpty(); - soft.assertThat(slotRepository.findByDateAndTimeAndTheme(FUTURE, time, otherTheme)).isEmpty(); - }); - } - } -} diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index 72ce64e10d..c9345ad117 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -24,9 +24,9 @@ import roomescape.domain.reservation.Status; import roomescape.domain.theme.Theme; import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.ReservationTimeJpaRepository; import roomescape.repository.SlotRepository; -import roomescape.repository.ThemeRepository; +import roomescape.repository.ThemeJpaRepository; @SpringBootTest(webEnvironment = WebEnvironment.NONE) @Transactional @@ -37,9 +37,9 @@ public class ReservationServiceTest { @Autowired private ReservationRepository reservationRepository; @Autowired - private ThemeRepository themeRepository; + private ThemeJpaRepository themeRepository; @Autowired - private ReservationTimeRepository reservationTimeRepository; + private ReservationTimeJpaRepository reservationTimeRepository; @Autowired private SlotRepository slotRepository; diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java index 125def282d..773a949aaa 100644 --- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java @@ -12,12 +12,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import roomescape.controller.dto.request.AvailableTimeFindRequest; import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.ReservationTimeJpaRepository; @ExtendWith(MockitoExtension.class) class ReservationTimeServiceTest { @Mock - private ReservationTimeRepository reservationTimeRepository; + private ReservationTimeJpaRepository reservationTimeRepository; @Mock private ReservationRepository reservationRepository; diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java index ca5880aca4..0b3fe8abbe 100644 --- a/src/test/java/roomescape/service/ThemeServiceTest.java +++ b/src/test/java/roomescape/service/ThemeServiceTest.java @@ -16,11 +16,14 @@ import roomescape.RoomEscapeFixture; import roomescape.controller.dto.request.ThemeFamousFindRequest; import roomescape.repository.ReservationRepository; +import roomescape.repository.ThemeJpaRepository; import roomescape.repository.ThemeRepository; @ExtendWith(MockitoExtension.class) class ThemeServiceTest { + @Mock + private ThemeJpaRepository themeJpaRepository; @Mock private ThemeRepository themeRepository; @@ -33,7 +36,7 @@ class ThemeServiceTest { @Test void 존재하지_않는_테마_조회_시_예외가_발생한다() { // given - given(themeRepository.findById(999L)).willReturn(Optional.empty()); + given(themeJpaRepository.findById(999L)).willReturn(Optional.empty()); // when & then Assertions.assertThatThrownBy(() -> themeService.find(999L)).isInstanceOf(RoomEscapeException.class); @@ -42,7 +45,7 @@ class ThemeServiceTest { @Test void 존재하지_않는_테마_삭제_시_예외가_발생한다() { // given - given(themeRepository.existsById(999L)).willReturn(false); + given(themeJpaRepository.existsById(999L)).willReturn(false); // when & then Assertions.assertThatThrownBy(() -> themeService.delete(999L)).isInstanceOf(RoomEscapeException.class); @@ -62,7 +65,7 @@ class ThemeServiceTest { @Test void 삭제시_테마를_사용하는_예외가_있으면_예외가_발생한다() { - given(themeRepository.existsById(1L)).willReturn(true); + given(themeJpaRepository.existsById(1L)).willReturn(true); given(reservationRepository.existsByThemeId(1L)).willReturn(true); Assertions.assertThatThrownBy(() -> themeService.delete(1L)).isInstanceOf(RoomEscapeException.class); } From 90bb618e69f3d1279fdf927d943ff2462d93fdb3 Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Wed, 17 Jun 2026 16:47:34 +0900 Subject: [PATCH 25/26] =?UTF-8?q?[2=20=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?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReservationController.java | 6 +++ .../domain/reservation/Reservation.java | 38 +++++++++++++++---- .../domain/reservation/ReservationDate.java | 9 ++++- .../domain/reservation/ReservationName.java | 9 ++++- .../domain/reservation/ReservationTime.java | 4 +- .../roomescape/domain/reservation/Slot.java | 36 ++++++++++++++---- .../java/roomescape/domain/theme/Theme.java | 2 +- .../repository/SlotJpaRepository.java | 9 +++++ .../service/ReservationService.java | 20 ++++++---- src/main/resources/application.properties | 5 ++- 10 files changed, 110 insertions(+), 28 deletions(-) create mode 100644 src/main/java/roomescape/repository/SlotJpaRepository.java diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java index 24b9171856..9f1f7bc1a9 100644 --- a/src/main/java/roomescape/controller/ReservationController.java +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -39,6 +39,7 @@ public ReservationResponse create(@Valid @RequestBody ReservationCreateRequest r @ResponseStatus(HttpStatus.OK) public List findList(@RequestParam(required = false) String name) { List reservations = reservationService.findList(name); + System.out.println("예약 건 수 : " + reservations.size()); return reservations.stream() .map(ReservationResponse::from) @@ -64,4 +65,9 @@ public ReservationResponse update(@Valid @RequestBody ReservationUpdateRequest r RankedReservation updated = reservationService.update(request, id, LocalDateTime.now()); return ReservationResponse.from(updated); } + + @GetMapping("/test") + public void test() { + reservationService.find(1L); + } } diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index dd554244d0..ce77e17d13 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -2,19 +2,43 @@ import common.exception.ErrorCode; import common.exception.RoomEscapeException; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +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; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import java.time.LocalDateTime; import java.util.Objects; import roomescape.domain.theme.Theme; +@Entity public class Reservation { private static final long TRANSIENT = 0L; - private final long id; - private final ReservationName name; - private final Slot slot; - private final Status status; - private final LocalDateTime createdAt; - private Reservation(long id, ReservationName name, Slot slot, Status status, LocalDateTime createdAt) { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Embedded + private ReservationName name; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "slot_id", nullable = false) + private Slot slot; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Status status; + @Column(nullable = false) + private LocalDateTime createdAt; + + protected Reservation() { + } + + private Reservation(Long id, ReservationName name, Slot slot, Status status, LocalDateTime createdAt) { this.id = id; this.name = Objects.requireNonNull(name); this.slot = Objects.requireNonNull(slot); @@ -22,7 +46,7 @@ private Reservation(long id, ReservationName name, Slot slot, Status status, Loc this.createdAt = Objects.requireNonNull(createdAt); } - public static Reservation load(long id, ReservationName reservationName, Slot slot, Status status, + public static Reservation load(Long id, ReservationName reservationName, Slot slot, Status status, LocalDateTime createdAt) { return new Reservation(id, reservationName, slot, status, createdAt); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationDate.java b/src/main/java/roomescape/domain/reservation/ReservationDate.java index fb33d303c1..be55531b02 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationDate.java +++ b/src/main/java/roomescape/domain/reservation/ReservationDate.java @@ -1,10 +1,17 @@ package roomescape.domain.reservation; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; import java.time.LocalDate; import java.util.Objects; +@Embeddable public class ReservationDate { - private final LocalDate value; + @Column(name = "date", nullable = false) + private LocalDate value; + + protected ReservationDate() { + } public ReservationDate(LocalDate value) { this.value = Objects.requireNonNull(value); diff --git a/src/main/java/roomescape/domain/reservation/ReservationName.java b/src/main/java/roomescape/domain/reservation/ReservationName.java index 76a9bcf31b..a000947705 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationName.java +++ b/src/main/java/roomescape/domain/reservation/ReservationName.java @@ -2,13 +2,17 @@ import common.exception.ErrorCode; import common.exception.RoomEscapeException; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; import java.util.Objects; +@Embeddable public class ReservationName { private static final int MIN_NAME_LENGTH = 1; private static final int MAX_NAME_LENGTH = 20; - private final String value; + @Column(name = "name", nullable = false) + private String value; public ReservationName(String value) { Objects.requireNonNull(value); @@ -17,6 +21,9 @@ public ReservationName(String value) { this.value = striped; } + protected ReservationName() { + } + private void validateLength(String value) { if (value.length() < MIN_NAME_LENGTH || value.length() > MAX_NAME_LENGTH) { throw new RoomEscapeException(ErrorCode.INVALID_NAME_LENGTH); diff --git a/src/main/java/roomescape/domain/reservation/ReservationTime.java b/src/main/java/roomescape/domain/reservation/ReservationTime.java index 8b5f5e2873..744c1ef2c3 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservation/ReservationTime.java @@ -14,14 +14,14 @@ public class ReservationTime { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; + private Long id; @Column(nullable = false) private LocalTime startAt; protected ReservationTime() { } - private ReservationTime(long id, LocalTime startAt) { + private ReservationTime(Long id, LocalTime startAt) { this.id = id; this.startAt = Objects.requireNonNull(startAt); } diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index 2d8fa73b76..a4f600fcf8 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -1,24 +1,44 @@ package roomescape.domain.reservation; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import java.time.LocalDateTime; import java.util.Objects; import roomescape.domain.theme.Theme; +@Entity public class Slot { - private static final int TRANSIENT = 0; - private final long id; - private final ReservationDate date; - private final ReservationTime time; - private final Theme theme; + private static final Long TRANSIENT = 0L; - private Slot(long id, ReservationDate date, ReservationTime time, Theme theme) { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Embedded + private ReservationDate date; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "time_id", nullable = false) + private ReservationTime time; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "theme_id", nullable = false) + private Theme theme; + + protected Slot() { + } + + private Slot(Long id, ReservationDate date, ReservationTime time, Theme theme) { this.id = id; this.date = Objects.requireNonNull(date); this.time = Objects.requireNonNull(time); this.theme = Objects.requireNonNull(theme); } - public static Slot load(long id, ReservationDate date, ReservationTime time, Theme theme) { + public static Slot load(Long id, ReservationDate date, ReservationTime time, Theme theme) { return new Slot(id, date, time, theme); } @@ -31,7 +51,7 @@ public Slot withId(long id) { } public boolean isSame(Slot target) { - return id == target.id; + return id.equals(target.id); } public boolean isBefore(LocalDateTime now) { diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index 5aaec61523..93dddefe1b 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -30,7 +30,7 @@ private Theme(Long id, ThemeName name, String description, ThumbnailUrl thumbnai this.thumbnailUrl = Objects.requireNonNull(thumbnailUrl); } - public static Theme load(long id, ThemeName name, String description, ThumbnailUrl thumbnailUrl) { + public static Theme load(Long id, ThemeName name, String description, ThumbnailUrl thumbnailUrl) { return new Theme(id, name, description, thumbnailUrl); } diff --git a/src/main/java/roomescape/repository/SlotJpaRepository.java b/src/main/java/roomescape/repository/SlotJpaRepository.java new file mode 100644 index 0000000000..94f482a2aa --- /dev/null +++ b/src/main/java/roomescape/repository/SlotJpaRepository.java @@ -0,0 +1,9 @@ +package roomescape.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import roomescape.domain.reservation.Slot; + +@Repository +public interface SlotJpaRepository extends JpaRepository { +} diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index ba4fc8e04e..49cc07b8d1 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -13,16 +13,21 @@ import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; -import roomescape.domain.reservation.Status; -import roomescape.repository.ReservationRepository; +import roomescape.repository.ReservationJpaRepository; @Service @Transactional(readOnly = true) public class ReservationService { private final SlotService slotService; - private final ReservationRepository reservationRepository; + private final ReservationJpaRepository reservationRepository; - public ReservationService(SlotService slotService, ReservationRepository reservationRepository) { +// public ReservationService(SlotService slotService, ReservationRepository reservationRepository) { +// this.slotService = slotService; +// this.reservationRepository = reservationRepository; +// } + + + public ReservationService(SlotService slotService, ReservationJpaRepository reservationRepository) { this.slotService = slotService; this.reservationRepository = reservationRepository; } @@ -66,7 +71,8 @@ public RankedReservation update(ReservationUpdateRequest request, long id, Local Reservations reservations = new Reservations(reservationRepository.findAllBySlot(updateSlot)); Reservation reserved = reservations.reserve(new ReservationName(request.getName()), updateSlot, now); - Reservation updated = reservationRepository.update(id, reserved); +// Reservation updated = reservationRepository.update(id, reserved); + Reservation updated = null; if (originReservation.isApproved()) { findFirstWaitingAndUpdateStatus(originReservation); @@ -76,8 +82,8 @@ public RankedReservation update(ReservationUpdateRequest request, long id, Local } private void findFirstWaitingAndUpdateStatus(Reservation reservation) { - reservationRepository.findFirstWaitingBySlot(reservation.getSlot()) - .ifPresent(waiting -> reservationRepository.updateStatus(waiting.getId(), Status.APPROVED)); +// reservationRepository.findFirstWaitingBySlot(reservation.getSlot()) +// .ifPresent(waiting -> reservationRepository.updateStatus(waiting.getId(), Status.APPROVED)); } @Transactional diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b2bce96e46..b8e5508f13 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,4 +6,7 @@ spring.datasource.url=jdbc:h2:mem:database spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.ddl-auto=create-drop -spring.jpa.defer-datasource-initialization=true \ No newline at end of file +spring.jpa.defer-datasource-initialization=true +# OSIV 종료 +#spring.jpa.open-in-view=false +server.port=8081 \ No newline at end of file From a7ba6a2ee85a356b8f2ccad57d68b68b7d3b6b1a Mon Sep 17 00:00:00 2001 From: alstj2384 Date: Thu, 18 Jun 2026 17:15:13 +0900 Subject: [PATCH 26/26] =?UTF-8?q?[3-4=20=EB=8B=A8=EA=B3=84]=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EB=8C=80=EA=B8=B0=20=EB=B0=8F=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + src/main/java/common/exception/ErrorCode.java | 17 +- .../java/common/exception/ErrorResponse.java | 17 +- .../common/exception/RoomEscapeException.java | 7 +- .../controller/GlobalExceptionHandler.java | 5 +- .../controller/ReservationController.java | 12 +- .../controller/ReservationTimeController.java | 6 +- .../controller/ThemeController.java | 6 +- .../dto/request/AvailableTimeFindRequest.java | 17 +- .../dto/request/ReservationCreateRequest.java | 27 +--- .../request/ReservationTimeCreateRequest.java | 12 +- .../dto/request/ReservationUpdateRequest.java | 27 +--- .../dto/request/ThemeCreateRequest.java | 22 +-- .../dto/request/ThemeFamousFindRequest.java | 22 +-- .../dto/response/ReservationResponse.java | 50 +----- .../dto/response/ReservationTimeResponse.java | 17 +- .../dto/response/ThemeResponse.java | 27 +--- .../roomescape/domain/reservation/Rank.java | 23 +-- .../domain/reservation/RankedReservation.java | 17 +- .../domain/reservation/Reservation.java | 86 +++------- .../domain/reservation/ReservationDate.java | 30 +--- .../domain/reservation/ReservationName.java | 30 +--- .../domain/reservation/ReservationTime.java | 23 +-- .../domain/reservation/Reservations.java | 22 +-- .../roomescape/domain/reservation/Slot.java | 31 +--- .../roomescape/domain/reservation/Status.java | 13 +- .../domain/theme/FamousThemeCondition.java | 14 +- .../java/roomescape/domain/theme/Theme.java | 27 +--- .../roomescape/domain/theme/ThemeName.java | 27 +--- .../roomescape/domain/theme/ThumbnailUrl.java | 27 +--- .../repository/RepositoryRowMapper.java | 50 ------ .../repository/ReservationRepository.java | 147 ++---------------- .../ReservationTimeJpaRepository.java | 9 -- .../repository/ReservationTimeRepository.java | 37 ++--- .../repository/SlotJpaRepository.java | 9 -- .../roomescape/repository/SlotRepository.java | 70 +-------- .../repository/ThemeJpaRepository.java | 9 -- .../repository/ThemeRepository.java | 44 ++---- .../service/ReservationService.java | 61 ++++---- .../service/ReservationTimeService.java | 37 ++--- .../java/roomescape/service/SlotService.java | 27 +--- .../java/roomescape/service/ThemeService.java | 37 ++--- src/main/resources/application.properties | 3 +- src/main/resources/data.sql | 131 ++++++++-------- .../java/roomescape/MissionStep2Test.java | 5 +- .../java/roomescape/RoomEscapeFixture.java | 19 ++- .../roomescape/RoomescapeApplicationTest.java | 52 ------- .../domain/reservation/ReservationTest.java | 18 --- .../domain/reservation/ReservationsTest.java | 2 +- .../service/ReservationServiceTest.java | 17 +- .../service/ReservationTimeServiceTest.java | 25 +-- .../roomescape/service/ThemeServiceTest.java | 24 +-- 52 files changed, 387 insertions(+), 1110 deletions(-) delete mode 100644 src/main/java/roomescape/repository/RepositoryRowMapper.java delete mode 100644 src/main/java/roomescape/repository/ReservationTimeJpaRepository.java delete mode 100644 src/main/java/roomescape/repository/SlotJpaRepository.java delete mode 100644 src/main/java/roomescape/repository/ThemeJpaRepository.java diff --git a/build.gradle b/build.gradle index 59b5993f0b..27946920e7 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' } diff --git a/src/main/java/common/exception/ErrorCode.java b/src/main/java/common/exception/ErrorCode.java index 09a2cfb795..b69ad0c64f 100644 --- a/src/main/java/common/exception/ErrorCode.java +++ b/src/main/java/common/exception/ErrorCode.java @@ -1,7 +1,11 @@ package common.exception; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +@RequiredArgsConstructor +@Getter public enum ErrorCode { // 요청 형식 및 필수값 위반 (BAD REQUEST) INVALID_NAME_LENGTH("이름 길이는 1자 ~ 20자 사이여야 합니다.", HttpStatus.BAD_REQUEST), @@ -28,17 +32,4 @@ public enum ErrorCode { private final String message; private final HttpStatus httpStatus; - - ErrorCode(String message, HttpStatus httpStatus) { - this.message = message; - this.httpStatus = httpStatus; - } - - public String getMessage() { - return message; - } - - public HttpStatus getHttpStatus() { - return httpStatus; - } } diff --git a/src/main/java/common/exception/ErrorResponse.java b/src/main/java/common/exception/ErrorResponse.java index bae947db10..6addd654cc 100644 --- a/src/main/java/common/exception/ErrorResponse.java +++ b/src/main/java/common/exception/ErrorResponse.java @@ -1,25 +1,16 @@ package common.exception; import java.time.LocalDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor +@Getter public class ErrorResponse { private final LocalDateTime timestamp; private final String message; - private ErrorResponse(LocalDateTime timestamp, String message) { - this.timestamp = timestamp; - this.message = message; - } - public static ErrorResponse create(String message) { return new ErrorResponse(LocalDateTime.now(), message); } - - public LocalDateTime getTimestamp() { - return timestamp; - } - - public String getMessage() { - return message; - } } diff --git a/src/main/java/common/exception/RoomEscapeException.java b/src/main/java/common/exception/RoomEscapeException.java index 3b88d8e0d5..ed54d7a2ab 100644 --- a/src/main/java/common/exception/RoomEscapeException.java +++ b/src/main/java/common/exception/RoomEscapeException.java @@ -1,5 +1,8 @@ package common.exception; +import lombok.Getter; + +@Getter public class RoomEscapeException extends RuntimeException { private final ErrorCode errorCode; @@ -7,8 +10,4 @@ public RoomEscapeException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } - - public ErrorCode getErrorCode() { - return errorCode; - } } diff --git a/src/main/java/roomescape/controller/GlobalExceptionHandler.java b/src/main/java/roomescape/controller/GlobalExceptionHandler.java index 0a9f8efb33..18206d8d6f 100644 --- a/src/main/java/roomescape/controller/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/controller/GlobalExceptionHandler.java @@ -2,8 +2,7 @@ import common.exception.ErrorResponse; import common.exception.RoomEscapeException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -14,10 +13,10 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice +@Slf4j public class GlobalExceptionHandler { public static final String UNEXPECTED_ERROR = "예상치 못한 오류가 발생했습니다. 잠시 후 다시 시도해주세요."; public static final String DATABASE_ERROR = "데이터 베이스 관련 오류가 발생했습니다. 관리자에게 문의해주세요"; - private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(RoomEscapeException.class) public ResponseEntity roomEscapeExceptionHandle(RoomEscapeException e) { diff --git a/src/main/java/roomescape/controller/ReservationController.java b/src/main/java/roomescape/controller/ReservationController.java index 9f1f7bc1a9..b927e3967d 100644 --- a/src/main/java/roomescape/controller/ReservationController.java +++ b/src/main/java/roomescape/controller/ReservationController.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import java.time.LocalDateTime; import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -20,13 +21,10 @@ import roomescape.service.ReservationService; @RestController +@RequiredArgsConstructor public class ReservationController { private final ReservationService reservationService; - public ReservationController(ReservationService reservationService) { - this.reservationService = reservationService; - } - @PostMapping("/reservations") @ResponseStatus(HttpStatus.CREATED) public ReservationResponse create(@Valid @RequestBody ReservationCreateRequest request) { @@ -39,7 +37,6 @@ public ReservationResponse create(@Valid @RequestBody ReservationCreateRequest r @ResponseStatus(HttpStatus.OK) public List findList(@RequestParam(required = false) String name) { List reservations = reservationService.findList(name); - System.out.println("예약 건 수 : " + reservations.size()); return reservations.stream() .map(ReservationResponse::from) @@ -65,9 +62,4 @@ public ReservationResponse update(@Valid @RequestBody ReservationUpdateRequest r RankedReservation updated = reservationService.update(request, id, LocalDateTime.now()); return ReservationResponse.from(updated); } - - @GetMapping("/test") - public void test() { - reservationService.find(1L); - } } diff --git a/src/main/java/roomescape/controller/ReservationTimeController.java b/src/main/java/roomescape/controller/ReservationTimeController.java index 69baccb4ba..cf66fb9349 100644 --- a/src/main/java/roomescape/controller/ReservationTimeController.java +++ b/src/main/java/roomescape/controller/ReservationTimeController.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import java.time.LocalDate; import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -19,13 +20,10 @@ import roomescape.service.ReservationTimeService; @RestController +@RequiredArgsConstructor public class ReservationTimeController { private final ReservationTimeService reservationTimeService; - public ReservationTimeController(ReservationTimeService reservationTimeService) { - this.reservationTimeService = reservationTimeService; - } - @PostMapping("/admin/times") @ResponseStatus(HttpStatus.CREATED) public ReservationTimeResponse create(@Valid @RequestBody ReservationTimeCreateRequest request) { diff --git a/src/main/java/roomescape/controller/ThemeController.java b/src/main/java/roomescape/controller/ThemeController.java index cb4d793d2a..54dfedcc68 100644 --- a/src/main/java/roomescape/controller/ThemeController.java +++ b/src/main/java/roomescape/controller/ThemeController.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import java.time.LocalDate; import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -19,13 +20,10 @@ import roomescape.service.ThemeService; @RestController +@RequiredArgsConstructor public class ThemeController { private final ThemeService themeService; - public ThemeController(ThemeService themeService) { - this.themeService = themeService; - } - @PostMapping("/admin/themes") @ResponseStatus(HttpStatus.CREATED) public ThemeResponse create(@Valid @RequestBody ThemeCreateRequest request) { diff --git a/src/main/java/roomescape/controller/dto/request/AvailableTimeFindRequest.java b/src/main/java/roomescape/controller/dto/request/AvailableTimeFindRequest.java index a90ddff4c2..1e4ab4f8bc 100644 --- a/src/main/java/roomescape/controller/dto/request/AvailableTimeFindRequest.java +++ b/src/main/java/roomescape/controller/dto/request/AvailableTimeFindRequest.java @@ -3,7 +3,11 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.time.LocalDate; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor +@Getter public class AvailableTimeFindRequest { @NotNull(message = "날짜는 필수로 입력해야 합니다.") private final LocalDate date; @@ -11,17 +15,4 @@ public class AvailableTimeFindRequest { @NotNull(message = "Theme ID는 필수로 입력해야 합니다.") @Positive(message = "Theme ID는 양수여야 합니다.") private final Long themeId; - - public AvailableTimeFindRequest(LocalDate date, Long themeId) { - this.date = date; - this.themeId = themeId; - } - - public LocalDate getDate() { - return date; - } - - public Long getThemeId() { - return themeId; - } } diff --git a/src/main/java/roomescape/controller/dto/request/ReservationCreateRequest.java b/src/main/java/roomescape/controller/dto/request/ReservationCreateRequest.java index 7cd5b43788..e7086c4c89 100644 --- a/src/main/java/roomescape/controller/dto/request/ReservationCreateRequest.java +++ b/src/main/java/roomescape/controller/dto/request/ReservationCreateRequest.java @@ -3,7 +3,11 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.time.LocalDate; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor +@Getter public class ReservationCreateRequest { @NotNull(message = "이름은 필수로 입력해야 합니다") private final String name; @@ -18,27 +22,4 @@ public class ReservationCreateRequest { @NotNull @Positive(message = "Theme ID는 양수여야 합니다.") private final Long themeId; - - public ReservationCreateRequest(String name, LocalDate date, Long timeId, Long themeId) { - this.name = name; - this.date = date; - this.timeId = timeId; - this.themeId = themeId; - } - - public String getName() { - return name; - } - - public LocalDate getDate() { - return date; - } - - public Long getTimeId() { - return timeId; - } - - public Long getThemeId() { - return themeId; - } } diff --git a/src/main/java/roomescape/controller/dto/request/ReservationTimeCreateRequest.java b/src/main/java/roomescape/controller/dto/request/ReservationTimeCreateRequest.java index 424e2b7085..827a537c20 100644 --- a/src/main/java/roomescape/controller/dto/request/ReservationTimeCreateRequest.java +++ b/src/main/java/roomescape/controller/dto/request/ReservationTimeCreateRequest.java @@ -2,16 +2,12 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor +@Getter public class ReservationTimeCreateRequest { @NotNull(message = "시간을 입력해야 합니다") private final LocalTime startAt; - - public ReservationTimeCreateRequest(LocalTime startAt) { - this.startAt = startAt; - } - - public LocalTime getStartAt() { - return startAt; - } } diff --git a/src/main/java/roomescape/controller/dto/request/ReservationUpdateRequest.java b/src/main/java/roomescape/controller/dto/request/ReservationUpdateRequest.java index 144097a270..c2c277f455 100644 --- a/src/main/java/roomescape/controller/dto/request/ReservationUpdateRequest.java +++ b/src/main/java/roomescape/controller/dto/request/ReservationUpdateRequest.java @@ -3,7 +3,11 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.time.LocalDate; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor +@Getter public class ReservationUpdateRequest { @NotNull(message = "이름은 필수로 입력해야 합니다") private final String name; @@ -18,27 +22,4 @@ public class ReservationUpdateRequest { @NotNull @Positive(message = "Theme ID는 양수여야 합니다.") private final Long themeId; - - public ReservationUpdateRequest(String name, LocalDate date, Long timeId, Long themeId) { - this.name = name; - this.date = date; - this.timeId = timeId; - this.themeId = themeId; - } - - public String getName() { - return name; - } - - public LocalDate getDate() { - return date; - } - - public Long getTimeId() { - return timeId; - } - - public Long getThemeId() { - return themeId; - } } diff --git a/src/main/java/roomescape/controller/dto/request/ThemeCreateRequest.java b/src/main/java/roomescape/controller/dto/request/ThemeCreateRequest.java index a283fdda73..0a4c761558 100644 --- a/src/main/java/roomescape/controller/dto/request/ThemeCreateRequest.java +++ b/src/main/java/roomescape/controller/dto/request/ThemeCreateRequest.java @@ -1,7 +1,11 @@ package roomescape.controller.dto.request; import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@Getter +@RequiredArgsConstructor public class ThemeCreateRequest { @NotNull(message = "이름은 필수로 입력해야 합니다") private final String name; @@ -11,22 +15,4 @@ public class ThemeCreateRequest { @NotNull(message = "URL은 필수로 입력해야 합니다") private final String thumbnailUrl; - - public ThemeCreateRequest(String name, String description, String thumbnailUrl) { - this.name = name; - this.description = description; - this.thumbnailUrl = thumbnailUrl; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getThumbnailUrl() { - return thumbnailUrl; - } } diff --git a/src/main/java/roomescape/controller/dto/request/ThemeFamousFindRequest.java b/src/main/java/roomescape/controller/dto/request/ThemeFamousFindRequest.java index 7f277c8543..cd6bc883b6 100644 --- a/src/main/java/roomescape/controller/dto/request/ThemeFamousFindRequest.java +++ b/src/main/java/roomescape/controller/dto/request/ThemeFamousFindRequest.java @@ -2,7 +2,11 @@ import jakarta.validation.constraints.Positive; import java.time.LocalDate; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@Getter +@RequiredArgsConstructor public class ThemeFamousFindRequest { @Positive(message = "기간은 양수여야 합니다") private final Long recentDays; @@ -11,24 +15,6 @@ public class ThemeFamousFindRequest { @Positive(message = "개수는 양수여야 합니다") private final Long limit; - - public ThemeFamousFindRequest(Long recentDays, LocalDate baseDate, Long limit) { - this.recentDays = recentDays; - this.baseDate = baseDate; - this.limit = limit; - } - - public Long getRecentDays() { - return recentDays; - } - - public LocalDate getBaseDate() { - return baseDate; - } - - public Long getLimit() { - return limit; - } } diff --git a/src/main/java/roomescape/controller/dto/response/ReservationResponse.java b/src/main/java/roomescape/controller/dto/response/ReservationResponse.java index 241717d186..2e57307f44 100644 --- a/src/main/java/roomescape/controller/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/controller/dto/response/ReservationResponse.java @@ -1,9 +1,13 @@ package roomescape.controller.dto.response; import java.time.LocalDate; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import roomescape.domain.reservation.RankedReservation; import roomescape.domain.reservation.Reservation; +@Getter +@RequiredArgsConstructor public class ReservationResponse { private final long id; private final String name; @@ -13,53 +17,13 @@ public class ReservationResponse { private final ReservationTimeResponse time; private final ThemeResponse theme; - public ReservationResponse(long id, String name, LocalDate date, String state, int rank, - ReservationTimeResponse time, - ThemeResponse theme) { - this.id = id; - this.name = name; - this.date = date; - this.state = state; - this.rank = rank; - this.time = time; - this.theme = theme; - } - public static ReservationResponse from(RankedReservation rankedReservation) { Reservation reservation = rankedReservation.getReservation(); return new ReservationResponse(reservation.getId(), reservation.getName().getValue(), - reservation.getDate().getValue(), + reservation.getSlot().getDate().getValue(), rankedReservation.getReservation().getStatus().getKoreanName(), rankedReservation.getRank().getValue(), - ReservationTimeResponse.from(reservation.getTime()), - ThemeResponse.from(reservation.getTheme())); - } - - public long getId() { - return id; - } - - public String getName() { - return name; - } - - public LocalDate getDate() { - return date; - } - - public ReservationTimeResponse getTime() { - return time; - } - - public ThemeResponse getTheme() { - return theme; - } - - public String getState() { - return state; - } - - public int getRank() { - return rank; + ReservationTimeResponse.from(reservation.getSlot().getTime()), + ThemeResponse.from(reservation.getSlot().getTheme())); } } diff --git a/src/main/java/roomescape/controller/dto/response/ReservationTimeResponse.java b/src/main/java/roomescape/controller/dto/response/ReservationTimeResponse.java index 4dc77234e6..1aa01413ea 100644 --- a/src/main/java/roomescape/controller/dto/response/ReservationTimeResponse.java +++ b/src/main/java/roomescape/controller/dto/response/ReservationTimeResponse.java @@ -1,26 +1,17 @@ package roomescape.controller.dto.response; import java.time.LocalTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import roomescape.domain.reservation.ReservationTime; +@Getter +@RequiredArgsConstructor public class ReservationTimeResponse { private final long id; private final LocalTime startAt; - public ReservationTimeResponse(long id, LocalTime startAt) { - this.id = id; - this.startAt = startAt; - } - public static ReservationTimeResponse from(ReservationTime reservationTime) { return new ReservationTimeResponse(reservationTime.getId(), reservationTime.getStartAt()); } - - public long getId() { - return id; - } - - public LocalTime getStartAt() { - return startAt; - } } diff --git a/src/main/java/roomescape/controller/dto/response/ThemeResponse.java b/src/main/java/roomescape/controller/dto/response/ThemeResponse.java index fbed5591a9..172ed8d15f 100644 --- a/src/main/java/roomescape/controller/dto/response/ThemeResponse.java +++ b/src/main/java/roomescape/controller/dto/response/ThemeResponse.java @@ -1,38 +1,19 @@ package roomescape.controller.dto.response; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import roomescape.domain.theme.Theme; +@Getter +@RequiredArgsConstructor public class ThemeResponse { private final long id; private final String name; private final String description; private final String thumbnailUrl; - public ThemeResponse(long id, String name, String description, String thumbnailUrl) { - this.id = id; - this.name = name; - this.description = description; - this.thumbnailUrl = thumbnailUrl; - } - public static ThemeResponse from(Theme theme) { return new ThemeResponse(theme.getId(), theme.getName().getValue(), theme.getDescription(), theme.getThumbnailUrl().getValue()); } - - public long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getThumbnailUrl() { - return thumbnailUrl; - } } diff --git a/src/main/java/roomescape/domain/reservation/Rank.java b/src/main/java/roomescape/domain/reservation/Rank.java index 7469f0a70b..359cd63b58 100644 --- a/src/main/java/roomescape/domain/reservation/Rank.java +++ b/src/main/java/roomescape/domain/reservation/Rank.java @@ -2,8 +2,11 @@ import common.exception.ErrorCode; import common.exception.RoomEscapeException; -import java.util.Objects; +import lombok.EqualsAndHashCode; +import lombok.Getter; +@Getter +@EqualsAndHashCode public class Rank { private static final int MIN_RANK_VALUE = 0; @@ -19,22 +22,4 @@ private void validate(int value) { throw new RoomEscapeException(ErrorCode.INVALID_RANK_VALUE); } } - - public int getValue() { - return value; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - Rank rank = (Rank) o; - return value == rank.value; - } - - @Override - public int hashCode() { - return Objects.hashCode(value); - } } diff --git a/src/main/java/roomescape/domain/reservation/RankedReservation.java b/src/main/java/roomescape/domain/reservation/RankedReservation.java index c938bd7800..7aa578f516 100644 --- a/src/main/java/roomescape/domain/reservation/RankedReservation.java +++ b/src/main/java/roomescape/domain/reservation/RankedReservation.java @@ -1,28 +1,19 @@ package roomescape.domain.reservation; import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor +@Getter public class RankedReservation { private final Rank rank; private final Reservation reservation; - private RankedReservation(Rank rank, Reservation reservation) { - this.rank = rank; - this.reservation = reservation; - } - public static RankedReservation decideRankFrom(Reservation target, List reservations) { long earlierCount = reservations.stream() .filter(r -> r.isEarlierThan(target)) .count(); return new RankedReservation(new Rank((int) earlierCount), target); } - - public Rank getRank() { - return rank; - } - - public Reservation getReservation() { - return reservation; - } } diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index ce77e17d13..a1c0ddc9a2 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -1,7 +1,5 @@ package roomescape.domain.reservation; -import common.exception.ErrorCode; -import common.exception.RoomEscapeException; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -15,12 +13,13 @@ import jakarta.persistence.ManyToOne; import java.time.LocalDateTime; import java.util.Objects; -import roomescape.domain.theme.Theme; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity +@NoArgsConstructor +@Getter public class Reservation { - private static final long TRANSIENT = 0L; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -35,9 +34,6 @@ public class Reservation { @Column(nullable = false) private LocalDateTime createdAt; - protected Reservation() { - } - private Reservation(Long id, ReservationName name, Slot slot, Status status, LocalDateTime createdAt) { this.id = id; this.name = Objects.requireNonNull(name); @@ -53,15 +49,26 @@ public static Reservation load(Long id, ReservationName reservationName, Slot sl public static Reservation create(ReservationName reservationName, Slot slot, Status status, LocalDateTime now) { Objects.requireNonNull(now); - Reservation reservation = new Reservation(TRANSIENT, reservationName, slot, status, now); - reservation.ensureNotPast(now); - return reservation; + return new Reservation(null, reservationName, slot, status, now); } - public void ensureNotPast(LocalDateTime now) { - if (slot.isBefore(now)) { - throw new RoomEscapeException(ErrorCode.PAST_RESERVATION_NOT_ALLOWED); - } + public boolean isPastThan(LocalDateTime now) { + return slot.isBefore(now); + } + + public void changeTo(Reservation target) { + this.name = target.name; + this.slot = target.slot; + this.status = target.status; + this.createdAt = target.createdAt; + } + + public void approve() { + this.status = Status.APPROVED; + } + + public boolean isApproved() { + return status == Status.APPROVED; } public boolean isEarlierThan(Reservation target) { @@ -80,51 +87,8 @@ public boolean hasSameSlot(Slot target) { return slot.isSame(target); } - public boolean isSameSlot(Slot target) { - return slot.isSame(target); - } - - public boolean isApproved() { - return status == Status.APPROVED; - } - - public boolean hasSameName(ReservationName name) { - return this.name.equals(name); - } - - public Reservation withId(long id) { - return new Reservation(id, name, slot, status, createdAt); - } - - public long getId() { - return id; - } - - public ReservationName getName() { - return name; - } - - public Slot getSlot() { - return slot; - } - - public ReservationDate getDate() { - return slot.getDate(); - } - - public ReservationTime getTime() { - return slot.getTime(); - } - - public Theme getTheme() { - return slot.getTheme(); - } - - public Status getStatus() { - return status; - } - - public LocalDateTime getCreatedAt() { - return createdAt; + public boolean hasSameName(ReservationName target) { + return name.equals(target); } } + diff --git a/src/main/java/roomescape/domain/reservation/ReservationDate.java b/src/main/java/roomescape/domain/reservation/ReservationDate.java index be55531b02..f5295e71c1 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationDate.java +++ b/src/main/java/roomescape/domain/reservation/ReservationDate.java @@ -4,37 +4,19 @@ import jakarta.persistence.Embeddable; import java.time.LocalDate; import java.util.Objects; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; @Embeddable +@NoArgsConstructor +@Getter +@EqualsAndHashCode public class ReservationDate { @Column(name = "date", nullable = false) private LocalDate value; - protected ReservationDate() { - } - public ReservationDate(LocalDate value) { this.value = Objects.requireNonNull(value); } - - public LocalDate getValue() { - return value; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ReservationDate that = (ReservationDate) o; - return Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hash(value); - } } diff --git a/src/main/java/roomescape/domain/reservation/ReservationName.java b/src/main/java/roomescape/domain/reservation/ReservationName.java index a000947705..aceb65fbdf 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationName.java +++ b/src/main/java/roomescape/domain/reservation/ReservationName.java @@ -5,8 +5,14 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.util.Objects; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; @Embeddable +@NoArgsConstructor +@Getter +@EqualsAndHashCode public class ReservationName { private static final int MIN_NAME_LENGTH = 1; private static final int MAX_NAME_LENGTH = 20; @@ -21,9 +27,6 @@ public ReservationName(String value) { this.value = striped; } - protected ReservationName() { - } - private void validateLength(String value) { if (value.length() < MIN_NAME_LENGTH || value.length() > MAX_NAME_LENGTH) { throw new RoomEscapeException(ErrorCode.INVALID_NAME_LENGTH); @@ -33,25 +36,4 @@ private void validateLength(String value) { public boolean isSame(String other) { return value.equals(other); } - - public String getValue() { - return value; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ReservationName reservationName = (ReservationName) o; - return Objects.equals(value, reservationName.value); - } - - @Override - public int hashCode() { - return Objects.hash(value); - } } diff --git a/src/main/java/roomescape/domain/reservation/ReservationTime.java b/src/main/java/roomescape/domain/reservation/ReservationTime.java index 744c1ef2c3..f14164f464 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservation/ReservationTime.java @@ -7,20 +7,19 @@ import jakarta.persistence.Id; import java.time.LocalTime; import java.util.Objects; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity +@NoArgsConstructor +@Getter public class ReservationTime { - private static final long TRANSIENT = 0L; - @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; this.startAt = Objects.requireNonNull(startAt); @@ -31,18 +30,6 @@ public static ReservationTime of(long id, LocalTime startAt) { } public static ReservationTime of(LocalTime startAt) { - return new ReservationTime(TRANSIENT, startAt); - } - - public ReservationTime withId(long id) { - return new ReservationTime(id, startAt); - } - - public long getId() { - return id; - } - - public LocalTime getStartAt() { - return startAt; + return new ReservationTime(null, startAt); } } diff --git a/src/main/java/roomescape/domain/reservation/Reservations.java b/src/main/java/roomescape/domain/reservation/Reservations.java index a9299053e0..bdc74f8e0b 100644 --- a/src/main/java/roomescape/domain/reservation/Reservations.java +++ b/src/main/java/roomescape/domain/reservation/Reservations.java @@ -35,28 +35,30 @@ private boolean hasNameAndSlot(ReservationName reservationName, Slot slot) { private Status decideStatusFor(Slot slot) { if (reservations.stream() - .filter(reservation -> reservation.isSameSlot(slot)) + .filter(reservation -> reservation.hasSameSlot(slot)) .anyMatch(reservation -> reservation.isApproved())) { return Status.WAITING; } return Status.APPROVED; } - public List rankedReservationsOf(String name) { - List listByName = findByName(name); - - return listByName.stream() - .map(this::toRankedReservation) - .toList(); - } - private List findByName(String name) { return reservations.stream() .filter(reservation -> reservation.hasSameName(new ReservationName(name))) .toList(); } - public List allRankedReservationsOf() { + public List rankedReservationsOf(String name) { + List target = reservations; + if (name != null) { + target = findByName(name); + } + return target.stream() + .map(this::toRankedReservation) + .toList(); + } + + public List rankedReservationsOf() { return reservations.stream() .map(this::toRankedReservation) .toList(); diff --git a/src/main/java/roomescape/domain/reservation/Slot.java b/src/main/java/roomescape/domain/reservation/Slot.java index a4f600fcf8..fc9cbda698 100644 --- a/src/main/java/roomescape/domain/reservation/Slot.java +++ b/src/main/java/roomescape/domain/reservation/Slot.java @@ -10,12 +10,14 @@ import jakarta.persistence.ManyToOne; import java.time.LocalDateTime; import java.util.Objects; +import lombok.Getter; +import lombok.NoArgsConstructor; import roomescape.domain.theme.Theme; @Entity +@NoArgsConstructor +@Getter public class Slot { - private static final Long TRANSIENT = 0L; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -28,9 +30,6 @@ public class Slot { @JoinColumn(name = "theme_id", nullable = false) private Theme theme; - protected Slot() { - } - private Slot(Long id, ReservationDate date, ReservationTime time, Theme theme) { this.id = id; this.date = Objects.requireNonNull(date); @@ -43,11 +42,7 @@ public static Slot load(Long id, ReservationDate date, ReservationTime time, The } public static Slot create(ReservationDate date, ReservationTime time, Theme theme) { - return new Slot(TRANSIENT, date, time, theme); - } - - public Slot withId(long id) { - return new Slot(id, date, time, theme); + return new Slot(null, date, time, theme); } public boolean isSame(Slot target) { @@ -59,20 +54,4 @@ public boolean isBefore(LocalDateTime now) { return reservationDateTime.isBefore(now); } - - public long getId() { - return id; - } - - public ReservationDate getDate() { - return date; - } - - public ReservationTime getTime() { - return time; - } - - public Theme getTheme() { - return theme; - } } diff --git a/src/main/java/roomescape/domain/reservation/Status.java b/src/main/java/roomescape/domain/reservation/Status.java index 1df323f658..24da7109f9 100644 --- a/src/main/java/roomescape/domain/reservation/Status.java +++ b/src/main/java/roomescape/domain/reservation/Status.java @@ -1,16 +1,13 @@ package roomescape.domain.reservation; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter public enum Status { APPROVED("승인"), WAITING("대기"); private final String koreanName; - - Status(String koreanName) { - this.koreanName = koreanName; - } - - public String getKoreanName() { - return koreanName; - } } diff --git a/src/main/java/roomescape/domain/theme/FamousThemeCondition.java b/src/main/java/roomescape/domain/theme/FamousThemeCondition.java index 65c0d4fd01..1b1a2232c1 100644 --- a/src/main/java/roomescape/domain/theme/FamousThemeCondition.java +++ b/src/main/java/roomescape/domain/theme/FamousThemeCondition.java @@ -2,7 +2,9 @@ import java.time.LocalDate; import java.util.Objects; +import lombok.Getter; +@Getter public class FamousThemeCondition { private static final long DEFAULT_DAYS = 7; private static final long DEFAULT_LIMIT = 10; @@ -26,16 +28,4 @@ public LocalDate startDate() { public LocalDate endDate() { return baseDate.minusDays(DEFAULT_GAP_DATE); } - - public Long getRecentDays() { - return recentDays; - } - - public LocalDate getBaseDate() { - return baseDate; - } - - public Long getLimit() { - return limit; - } } diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index 93dddefe1b..83aeb69313 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -7,8 +7,12 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import java.util.Objects; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity +@NoArgsConstructor +@Getter public class Theme { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -20,9 +24,6 @@ public class Theme { @Embedded private ThumbnailUrl thumbnailUrl; - protected Theme() { - } - private Theme(Long id, ThemeName name, String description, ThumbnailUrl thumbnailUrl) { this.id = id; this.name = Objects.requireNonNull(name); @@ -37,24 +38,4 @@ public static Theme load(Long id, ThemeName name, String description, ThumbnailU public static Theme create(ThemeName name, String description, ThumbnailUrl thumbnailUrl) { return new Theme(null, name, description, thumbnailUrl); } - - public Theme withId(long id) { - return new Theme(id, name, description, thumbnailUrl); - } - - public long getId() { - return id; - } - - public ThemeName getName() { - return name; - } - - public String getDescription() { - return description; - } - - public ThumbnailUrl getThumbnailUrl() { - return thumbnailUrl; - } } diff --git a/src/main/java/roomescape/domain/theme/ThemeName.java b/src/main/java/roomescape/domain/theme/ThemeName.java index 9632887ea6..92efa32047 100644 --- a/src/main/java/roomescape/domain/theme/ThemeName.java +++ b/src/main/java/roomescape/domain/theme/ThemeName.java @@ -5,8 +5,14 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.util.Objects; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; @Embeddable +@NoArgsConstructor +@Getter +@EqualsAndHashCode public class ThemeName { private static final int MIN_NAME_LENGTH = 1; private static final int MAX_NAME_LENGTH = 30; @@ -14,9 +20,6 @@ public class ThemeName { @Column(name = "name", nullable = false, length = 20) private String value; - protected ThemeName() { - } - public ThemeName(String value) { Objects.requireNonNull(value); String striped = value.strip(); @@ -29,22 +32,4 @@ private void validate(String value) { throw new RoomEscapeException(ErrorCode.INVALID_THEME_NAME_LENGTH); } } - - public String getValue() { - return value; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - ThemeName themeName = (ThemeName) o; - return Objects.equals(value, themeName.value); - } - - @Override - public int hashCode() { - return Objects.hashCode(value); - } } diff --git a/src/main/java/roomescape/domain/theme/ThumbnailUrl.java b/src/main/java/roomescape/domain/theme/ThumbnailUrl.java index 3bb49cd69b..cf51997695 100644 --- a/src/main/java/roomescape/domain/theme/ThumbnailUrl.java +++ b/src/main/java/roomescape/domain/theme/ThumbnailUrl.java @@ -6,17 +6,20 @@ import jakarta.persistence.Embeddable; import java.util.Objects; import java.util.regex.Pattern; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; @Embeddable +@NoArgsConstructor +@Getter +@EqualsAndHashCode public class ThumbnailUrl { private static final Pattern URL_PATTERN = Pattern.compile("^https?://.+"); @Column(name = "thumbnail_url", nullable = false) private String value; - protected ThumbnailUrl() { - } - public ThumbnailUrl(String value) { validate(value); this.value = value; @@ -28,22 +31,4 @@ private void validate(String value) { throw new RoomEscapeException(ErrorCode.INVALID_THUMBNAIL_URL); } } - - public String getValue() { - return value; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - ThumbnailUrl that = (ThumbnailUrl) o; - return Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hashCode(value); - } } diff --git a/src/main/java/roomescape/repository/RepositoryRowMapper.java b/src/main/java/roomescape/repository/RepositoryRowMapper.java deleted file mode 100644 index eebf9c42a1..0000000000 --- a/src/main/java/roomescape/repository/RepositoryRowMapper.java +++ /dev/null @@ -1,50 +0,0 @@ -package roomescape.repository; - -import java.sql.ResultSet; -import java.sql.SQLException; -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; -import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeName; -import roomescape.domain.theme.ThumbnailUrl; - -public final class RepositoryRowMapper { - private RepositoryRowMapper() { - } - - public static Reservation reservationRowMapper(ResultSet rs) throws SQLException { - return Reservation.load( - rs.getLong("reservation_id"), - new ReservationName(rs.getString("name")), - slotRowMapper(rs), - Status.valueOf(rs.getString("status")), - rs.getTimestamp("created_at").toLocalDateTime() - ); - } - - public static Slot slotRowMapper(ResultSet rs) throws SQLException { - return Slot.load( - rs.getLong("slot_id"), - new ReservationDate(rs.getDate("slot_date").toLocalDate()), - reservationTimeRowMapper(rs), - themeRowMapper(rs) - ); - } - - public static ReservationTime reservationTimeRowMapper(ResultSet rs) throws SQLException { - return ReservationTime.of(rs.getLong("time_id"), rs.getTime("start_at").toLocalTime()); - } - - public static Theme themeRowMapper(ResultSet rs) throws SQLException { - return Theme.load( - rs.getLong("theme_id"), - new ThemeName(rs.getString("theme_name")), - rs.getString("description"), - new ThumbnailUrl(rs.getString("thumbnail_url")) - ); - } -} diff --git a/src/main/java/roomescape/repository/ReservationRepository.java b/src/main/java/roomescape/repository/ReservationRepository.java index 2a8281aa81..bf3cfd822d 100644 --- a/src/main/java/roomescape/repository/ReservationRepository.java +++ b/src/main/java/roomescape/repository/ReservationRepository.java @@ -1,144 +1,25 @@ package roomescape.repository; import java.util.List; -import java.util.Map; import java.util.Optional; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.Slot; import roomescape.domain.reservation.Status; @Repository -public class ReservationRepository { - public static final RowMapper RESERVATION_ROW_MAPPER = - (rs, rowNum) -> RepositoryRowMapper.reservationRowMapper(rs); - private static final String SELECT_ALL = """ - SELECT r.id AS reservation_id, - r.name, - r.status, - r.created_at, - s.id AS slot_id, - s.date AS slot_date, - rt.id AS time_id, - rt.start_at, - t.id AS theme_id, - t.name AS theme_name, - t.description, - t.thumbnail_url - FROM reservation r - INNER JOIN slot s ON r.slot_id = s.id - INNER JOIN reservation_time rt ON s.time_id = rt.id - INNER JOIN theme t ON s.theme_id = t.id - """; - private static final String UPDATE = """ - UPDATE reservation - SET name = ?, slot_id = ?, created_at = ? - WHERE id = ? - """; - private static final String SELECT_BY_ID = SELECT_ALL + "WHERE r.id = ?"; - private static final String SELECT_BY_SLOT = SELECT_ALL + "WHERE r.slot_id = ?"; - private static final String EXISTS_BY_SLOT_AND_NAME = """ - SELECT EXISTS ( - SELECT 1 - FROM reservation - WHERE slot_id = ? AND name = ? - ) - """; - private static final String SELECT_FIRST_WAITING_BY_SLOT = SELECT_ALL + """ - WHERE r.slot_id = ? AND r.status = 'WAITING' - ORDER BY r.created_at, r.id - LIMIT 1 - """; - private static final String UPDATE_STATUS = "UPDATE reservation SET status = ? WHERE id = ?"; - private static final String EXISTS_BY_TIME_ID = """ - SELECT EXISTS ( - SELECT 1 - FROM reservation r - INNER JOIN slot s ON r.slot_id = s.id - WHERE s.time_id = ? - ) - """; - private static final String EXISTS_BY_THEME_ID = """ - SELECT EXISTS ( - SELECT 1 - FROM reservation r - INNER JOIN slot s ON r.slot_id = s.id - WHERE s.theme_id = ? - ) - """; - - private final JdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert simpleJdbcInsert; - - public ReservationRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) - .withTableName("reservation") - .usingGeneratedKeyColumns("id"); - } - - public List findAll() { - return jdbcTemplate.query(SELECT_ALL, RESERVATION_ROW_MAPPER); - } - - public Optional findById(long reservationId) { - List result = jdbcTemplate.query(SELECT_BY_ID, RESERVATION_ROW_MAPPER, reservationId); - return result.stream().findFirst(); - } - - public List findAllBySlot(Slot foundSlot) { - return jdbcTemplate.query(SELECT_BY_SLOT, RESERVATION_ROW_MAPPER, foundSlot.getId()); - } - - public Optional findFirstWaitingBySlot(Slot slot) { - List result = jdbcTemplate.query(SELECT_FIRST_WAITING_BY_SLOT, RESERVATION_ROW_MAPPER, - slot.getId()); - return result.stream().findFirst(); - } - - public Reservation save(Reservation reservation) { - Map params = Map.of( - "name", reservation.getName().getValue(), - "slot_id", reservation.getSlot().getId(), - "status", reservation.getStatus().name(), - "created_at", reservation.getCreatedAt() - ); - - long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - - return reservation.withId(generatedKey); - } - - public Reservation update(long id, Reservation reservation) { - jdbcTemplate.update(UPDATE, reservation.getName().getValue(), reservation.getSlot().getId(), - reservation.getCreatedAt(), id); - - return reservation.withId(id); - } - - public void updateStatus(Long id, Status status) { - jdbcTemplate.update(UPDATE_STATUS, status.name(), id); - } - - public void deleteById(Long id) { - jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id); - } - - public boolean existsByTimeId(long reservationTimeId) { - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(EXISTS_BY_TIME_ID, Boolean.class, reservationTimeId)); - } - - public boolean existsByThemeId(long themeId) { - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(EXISTS_BY_THEME_ID, Boolean.class, themeId)); - } - - public boolean existsBySlotAndName(Slot slot, String name) { - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(EXISTS_BY_SLOT_AND_NAME, Boolean.class, slot.getId(), name)); - } +public interface ReservationRepository extends JpaRepository { + List findAllBySlot(Slot slot); + + @Query(""" + SELECT r FROM Reservation r + JOIN FETCH r.slot s + JOIN FETCH s.time + JOIN FETCH s.theme + """) + List findAll(); + + Optional findFirstBySlotAndStatusOrderByCreatedAtAscIdAsc(Slot slot, Status status); } diff --git a/src/main/java/roomescape/repository/ReservationTimeJpaRepository.java b/src/main/java/roomescape/repository/ReservationTimeJpaRepository.java deleted file mode 100644 index 59697926e8..0000000000 --- a/src/main/java/roomescape/repository/ReservationTimeJpaRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package roomescape.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservation.ReservationTime; - -@Repository -public interface ReservationTimeJpaRepository extends JpaRepository { -} diff --git a/src/main/java/roomescape/repository/ReservationTimeRepository.java b/src/main/java/roomescape/repository/ReservationTimeRepository.java index 817029f6d5..25cc880e65 100644 --- a/src/main/java/roomescape/repository/ReservationTimeRepository.java +++ b/src/main/java/roomescape/repository/ReservationTimeRepository.java @@ -2,33 +2,20 @@ import java.time.LocalDate; import java.util.List; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import roomescape.domain.reservation.ReservationTime; @Repository -public class ReservationTimeRepository { - private static final RowMapper RESERVATION_TIME_ROW_MAPPER = - (rs, rowNum) -> RepositoryRowMapper.reservationTimeRowMapper(rs); - - private final JdbcTemplate jdbcTemplate; - - public ReservationTimeRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findByDateAndTheme(LocalDate date, long themeId) { - String sql = """ - SELECT rt.id AS time_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 = ? AND s.theme_id = ? - ) - """; - return jdbcTemplate.query(sql, RESERVATION_TIME_ROW_MAPPER, date, themeId); - } +public interface ReservationTimeRepository extends JpaRepository { + @Query(""" + SELECT rt FROM ReservationTime rt + WHERE rt.id NOT IN ( + SELECT s.time.id FROM Reservation r + JOIN r.slot s + WHERE s.date.value = :date AND s.theme.id = :themeId + ) + """) + List findAvailableByDateAndTheme(LocalDate date, long themeId); } diff --git a/src/main/java/roomescape/repository/SlotJpaRepository.java b/src/main/java/roomescape/repository/SlotJpaRepository.java deleted file mode 100644 index 94f482a2aa..0000000000 --- a/src/main/java/roomescape/repository/SlotJpaRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package roomescape.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservation.Slot; - -@Repository -public interface SlotJpaRepository extends JpaRepository { -} diff --git a/src/main/java/roomescape/repository/SlotRepository.java b/src/main/java/roomescape/repository/SlotRepository.java index 05a4d8f990..9f646ef1e9 100644 --- a/src/main/java/roomescape/repository/SlotRepository.java +++ b/src/main/java/roomescape/repository/SlotRepository.java @@ -1,76 +1,18 @@ package roomescape.repository; -import java.time.LocalDate; -import java.util.List; -import java.util.Map; import java.util.Optional; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import roomescape.domain.reservation.ReservationDate; import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.theme.Theme; @Repository -public class SlotRepository { - private static final String SELECT_ALL = """ - SELECT s.id AS slot_id, - s.date AS slot_date, - rt.id AS time_id, - rt.start_at, - t.id AS theme_id, - t.name AS theme_name, - t.description, - t.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 static final RowMapper SLOT_ROW_MAPPER = - (rs, rowNum) -> RepositoryRowMapper.slotRowMapper(rs); +public interface SlotRepository extends JpaRepository { + Optional findByDateAndTimeAndTheme(ReservationDate date, ReservationTime time, Theme theme); - private final JdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert simpleJdbcInsert; + Optional findByTheme(Theme theme); - public SlotRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) - .withTableName("slot") - .usingGeneratedKeyColumns("id"); - } - - public Slot save(Slot slot) { - Map params = Map.of( - "date", slot.getDate().getValue(), - "time_id", slot.getTime().getId(), - "theme_id", slot.getTheme().getId() - ); - long generatedKey = simpleJdbcInsert.executeAndReturnKey(params).longValue(); - return slot.withId(generatedKey); - } - - public Optional findById(long id) { - String sql = SELECT_ALL + "WHERE s.id = ?"; - List result = jdbcTemplate.query(sql, SLOT_ROW_MAPPER, id); - return result.stream().findFirst(); - } - - public Optional findByDateAndTimeAndTheme(LocalDate date, ReservationTime time, Theme theme) { - String sql = SELECT_ALL + "WHERE s.date = ? AND s.time_id = ? AND s.theme_id = ?"; - List result = jdbcTemplate.query(sql, SLOT_ROW_MAPPER, date, time.getId(), theme.getId()); - return result.stream().findFirst(); - } - - public boolean lockSlot(Slot foundSlot) { - String sql = """ - SELECT 1 - FROM slot - WHERE id = ? - FOR UPDATE - """; - - List result = jdbcTemplate.queryForList(sql, Long.class, foundSlot.getId()); - return !result.isEmpty(); - } + Optional findByTime(ReservationTime time); } diff --git a/src/main/java/roomescape/repository/ThemeJpaRepository.java b/src/main/java/roomescape/repository/ThemeJpaRepository.java deleted file mode 100644 index 12470f79cb..0000000000 --- a/src/main/java/roomescape/repository/ThemeJpaRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package roomescape.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import roomescape.domain.theme.Theme; - -@Repository -public interface ThemeJpaRepository extends JpaRepository { -} diff --git a/src/main/java/roomescape/repository/ThemeRepository.java b/src/main/java/roomescape/repository/ThemeRepository.java index 55a03c9d84..c0ae2a5ab4 100644 --- a/src/main/java/roomescape/repository/ThemeRepository.java +++ b/src/main/java/roomescape/repository/ThemeRepository.java @@ -1,39 +1,21 @@ package roomescape.repository; +import java.time.LocalDate; import java.util.List; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import roomescape.domain.theme.FamousThemeCondition; import roomescape.domain.theme.Theme; @Repository -public class ThemeRepository { - public static final RowMapper THEME_ROW_MAPPER = (rs, rowNum) -> RepositoryRowMapper.themeRowMapper(rs); - - private final JdbcTemplate jdbcTemplate; - - public ThemeRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findFamous(FamousThemeCondition condition) { - String sql = """ - SELECT t.id AS theme_id, t.name AS theme_name, t.description, t.thumbnail_url - FROM THEME AS t - INNER JOIN ( - SELECT s.theme_id, count(s.theme_id) AS cnt - FROM reservation r - INNER JOIN slot s ON r.slot_id = s.id - WHERE s.date BETWEEN ? AND ? - GROUP BY s.theme_id - ORDER BY count(s.theme_id) DESC, s.theme_id DESC - LIMIT ? - ) AS topN ON t.id = topN.theme_id - ORDER BY topN.cnt DESC, topN.theme_id DESC - """; - - return jdbcTemplate.query(sql, THEME_ROW_MAPPER, condition.startDate(), condition.endDate(), - condition.getLimit()); - } +public interface ThemeRepository extends JpaRepository { + @Query(""" + SELECT s.theme FROM Reservation r + JOIN r.slot s + WHERE s.date.value BETWEEN :startDate AND :endDate + GROUP BY s.theme + ORDER BY COUNT(s.theme) DESC, s.theme.id DESC + """) + List findFamous(LocalDate startDate, LocalDate endDate, Pageable pageable); } diff --git a/src/main/java/roomescape/service/ReservationService.java b/src/main/java/roomescape/service/ReservationService.java index 49cc07b8d1..84fa261791 100644 --- a/src/main/java/roomescape/service/ReservationService.java +++ b/src/main/java/roomescape/service/ReservationService.java @@ -4,6 +4,7 @@ import common.exception.RoomEscapeException; import java.time.LocalDateTime; import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.controller.dto.request.ReservationCreateRequest; @@ -13,33 +14,25 @@ import roomescape.domain.reservation.ReservationName; import roomescape.domain.reservation.Reservations; import roomescape.domain.reservation.Slot; -import roomescape.repository.ReservationJpaRepository; +import roomescape.domain.reservation.Status; +import roomescape.repository.ReservationRepository; @Service @Transactional(readOnly = true) +@RequiredArgsConstructor public class ReservationService { private final SlotService slotService; - private final ReservationJpaRepository reservationRepository; - -// public ReservationService(SlotService slotService, ReservationRepository reservationRepository) { -// this.slotService = slotService; -// this.reservationRepository = reservationRepository; -// } - - - public ReservationService(SlotService slotService, ReservationJpaRepository reservationRepository) { - this.slotService = slotService; - this.reservationRepository = reservationRepository; - } + private final ReservationRepository reservationRepository; @Transactional public RankedReservation reserve(ReservationCreateRequest request, LocalDateTime now) { Slot foundSlot = slotService.findOrCreate(request.getDate(), request.getTimeId(), request.getThemeId()); - slotService.lockSlot(foundSlot); Reservations reservations = new Reservations(reservationRepository.findAllBySlot(foundSlot)); Reservation reservation = reservations.reserve(new ReservationName(request.getName()), foundSlot, now); + validateIsPastReservation(reservation, now); + return getRankedReservation(reservationRepository.save(reservation)); } @@ -52,49 +45,55 @@ public RankedReservation find(long reservationId) { } public List findList(String name) { - Reservations rankedReservations = new Reservations(reservationRepository.findAll()); + Reservations reservations = new Reservations(reservationRepository.findAll()); - if (name == null) { - return rankedReservations.allRankedReservationsOf(); - } - return rankedReservations.rankedReservationsOf(name); + return reservations.rankedReservationsOf(name); } @Transactional public RankedReservation update(ReservationUpdateRequest request, long id, LocalDateTime now) { Reservation originReservation = findReservationById(id); - originReservation.ensureNotPast(now); + + validateIsPastReservation(originReservation, now); + + Slot originSlot = originReservation.getSlot(); + boolean wasApproved = originReservation.isApproved(); Slot updateSlot = slotService.findOrCreate(request.getDate(), request.getTimeId(), request.getThemeId()); - slotService.lockSlot(updateSlot); Reservations reservations = new Reservations(reservationRepository.findAllBySlot(updateSlot)); Reservation reserved = reservations.reserve(new ReservationName(request.getName()), updateSlot, now); -// Reservation updated = reservationRepository.update(id, reserved); - Reservation updated = null; + originReservation.changeTo(reserved); - if (originReservation.isApproved()) { - findFirstWaitingAndUpdateStatus(originReservation); + if (wasApproved) { + promoteFirstWaiting(originSlot); } - return getRankedReservation(updated); + return getRankedReservation(originReservation); + } + + private void validateIsPastReservation(Reservation reservation, LocalDateTime now) { + if (reservation.isPastThan(now)) { + throw new RoomEscapeException(ErrorCode.PAST_RESERVATION_NOT_ALLOWED); + } } - private void findFirstWaitingAndUpdateStatus(Reservation reservation) { -// reservationRepository.findFirstWaitingBySlot(reservation.getSlot()) -// .ifPresent(waiting -> reservationRepository.updateStatus(waiting.getId(), Status.APPROVED)); + private void promoteFirstWaiting(Slot slot) { + reservationRepository.findFirstBySlotAndStatusOrderByCreatedAtAscIdAsc(slot, Status.WAITING) + .ifPresent(Reservation::approve); } @Transactional public void cancel(long reservationId, LocalDateTime now) { Reservation reservation = findReservationById(reservationId); - reservation.ensureNotPast(now); + + validateIsPastReservation(reservation, now); reservationRepository.deleteById(reservationId); if (reservation.isApproved()) { - findFirstWaitingAndUpdateStatus(reservation); + promoteFirstWaiting(reservation.getSlot()); } } diff --git a/src/main/java/roomescape/service/ReservationTimeService.java b/src/main/java/roomescape/service/ReservationTimeService.java index eec798ad00..74e42b156e 100644 --- a/src/main/java/roomescape/service/ReservationTimeService.java +++ b/src/main/java/roomescape/service/ReservationTimeService.java @@ -4,58 +4,49 @@ import common.exception.RoomEscapeException; import java.time.LocalDate; import java.util.List; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.controller.dto.request.AvailableTimeFindRequest; import roomescape.controller.dto.request.ReservationTimeCreateRequest; import roomescape.domain.reservation.ReservationTime; -import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeJpaRepository; import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.SlotRepository; @Service @Transactional(readOnly = true) +@RequiredArgsConstructor public class ReservationTimeService { private final ReservationTimeRepository reservationTimeRepository; - private final ReservationTimeJpaRepository reservationTimeJpaRepository; - private final ReservationRepository reservationRepository; - - public ReservationTimeService(ReservationTimeRepository reservationTimeRepository, - ReservationTimeJpaRepository reservationTimeJpaRepository, - ReservationRepository reservationRepository) { - this.reservationTimeRepository = reservationTimeRepository; - this.reservationTimeJpaRepository = reservationTimeJpaRepository; - this.reservationRepository = reservationRepository; - } + private final SlotRepository slotRepository; @Transactional public ReservationTime create(ReservationTimeCreateRequest request) { ReservationTime reservationTime = ReservationTime.of(request.getStartAt()); - return reservationTimeJpaRepository.save(reservationTime); + return reservationTimeRepository.save(reservationTime); } public List findAll() { - return reservationTimeJpaRepository.findAll(); + return reservationTimeRepository.findAll(); } public List findAvailable(AvailableTimeFindRequest request, LocalDate now) { if (now.isAfter(request.getDate())) { throw new RoomEscapeException(ErrorCode.PAST_DATE_NOT_ALLOWED); } - - return reservationTimeRepository.findByDateAndTheme(request.getDate(), request.getThemeId()); + return reservationTimeRepository.findAvailableByDateAndTheme(request.getDate(), request.getThemeId()); } @Transactional public void delete(long reservationTimeId) { - if (!reservationTimeJpaRepository.existsById(reservationTimeId)) { - throw new RoomEscapeException(ErrorCode.RESERVATION_TIME_NOT_FOUND); - } + ReservationTime reservationTime = reservationTimeRepository.findById(reservationTimeId) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.RESERVATION_TIME_NOT_FOUND)); - if (reservationRepository.existsByTimeId(reservationTimeId)) { - throw new RoomEscapeException(ErrorCode.RESERVATION_TIME_IN_USE); - } + slotRepository.findByTime(reservationTime) + .ifPresent(slot -> { + throw new RoomEscapeException(ErrorCode.RESERVATION_TIME_IN_USE); + }); - reservationTimeJpaRepository.deleteById(reservationTimeId); + reservationTimeRepository.deleteById(reservationTimeId); } } diff --git a/src/main/java/roomescape/service/SlotService.java b/src/main/java/roomescape/service/SlotService.java index 1a05363e12..895125d0fc 100644 --- a/src/main/java/roomescape/service/SlotService.java +++ b/src/main/java/roomescape/service/SlotService.java @@ -3,6 +3,7 @@ import common.exception.ErrorCode; import common.exception.RoomEscapeException; import java.time.LocalDate; +import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,23 +11,17 @@ import roomescape.domain.reservation.ReservationTime; import roomescape.domain.reservation.Slot; import roomescape.domain.theme.Theme; -import roomescape.repository.ReservationTimeJpaRepository; +import roomescape.repository.ReservationTimeRepository; import roomescape.repository.SlotRepository; -import roomescape.repository.ThemeJpaRepository; +import roomescape.repository.ThemeRepository; @Service @Transactional(readOnly = true) +@RequiredArgsConstructor public class SlotService { private final SlotRepository slotRepository; - private final ReservationTimeJpaRepository reservationTimeRepository; - private final ThemeJpaRepository themeRepository; - - public SlotService(SlotRepository slotRepository, ReservationTimeJpaRepository reservationTimeRepository, - ThemeJpaRepository themeRepository) { - this.slotRepository = slotRepository; - this.reservationTimeRepository = reservationTimeRepository; - this.themeRepository = themeRepository; - } + private final ReservationTimeRepository reservationTimeRepository; + private final ThemeRepository themeRepository; @Transactional public Slot findOrCreate(LocalDate date, long timeId, long themeId) { @@ -35,7 +30,7 @@ public Slot findOrCreate(LocalDate date, long timeId, long themeId) { Theme theme = themeRepository.findById(themeId) .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); - return slotRepository.findByDateAndTimeAndTheme(date, time, theme) + return slotRepository.findByDateAndTimeAndTheme(new ReservationDate(date), time, theme) .orElseGet(() -> saveOrReread(date, time, theme)); } @@ -43,14 +38,8 @@ private Slot saveOrReread(LocalDate date, ReservationTime time, Theme theme) { try { return slotRepository.save(Slot.create(new ReservationDate(date), time, theme)); } catch (DataIntegrityViolationException e) { - return slotRepository.findByDateAndTimeAndTheme(date, time, theme) + return slotRepository.findByDateAndTimeAndTheme(new ReservationDate(date), time, theme) .orElseThrow(() -> new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND)); } } - - public void lockSlot(Slot foundSlot) { - if (!slotRepository.lockSlot(foundSlot)) { - throw new RoomEscapeException(ErrorCode.SLOT_NOT_FOUND); - } - } } diff --git a/src/main/java/roomescape/service/ThemeService.java b/src/main/java/roomescape/service/ThemeService.java index faa7e58e2d..8bcbf5c80c 100644 --- a/src/main/java/roomescape/service/ThemeService.java +++ b/src/main/java/roomescape/service/ThemeService.java @@ -4,6 +4,9 @@ import common.exception.RoomEscapeException; import java.time.LocalDate; import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.controller.dto.request.ThemeCreateRequest; @@ -12,57 +15,49 @@ import roomescape.domain.theme.Theme; import roomescape.domain.theme.ThemeName; import roomescape.domain.theme.ThumbnailUrl; -import roomescape.repository.ReservationRepository; -import roomescape.repository.ThemeJpaRepository; +import roomescape.repository.SlotRepository; import roomescape.repository.ThemeRepository; @Service @Transactional(readOnly = true) +@RequiredArgsConstructor public class ThemeService { - private final ThemeJpaRepository themeJpaRepository; private final ThemeRepository themeRepository; - private final ReservationRepository reservationRepository; - - public ThemeService(ThemeJpaRepository themeJpaRepository, ThemeRepository themeRepository, - ReservationRepository reservationRepository) { - this.themeJpaRepository = themeJpaRepository; - this.themeRepository = themeRepository; - this.reservationRepository = reservationRepository; - } + private final SlotRepository slotRepository; @Transactional public Theme create(ThemeCreateRequest request) { Theme theme = Theme.create(new ThemeName(request.getName()), request.getDescription(), new ThumbnailUrl(request.getThumbnailUrl())); - return themeJpaRepository.save(theme); + return themeRepository.save(theme); } public Theme find(long themeId) { - return themeJpaRepository.findById(themeId) + return themeRepository.findById(themeId) .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); } public List findAll() { - return themeJpaRepository.findAll(); + return themeRepository.findAll(); } public List findFamous(ThemeFamousFindRequest request, LocalDate now) { FamousThemeCondition condition = new FamousThemeCondition(request.getRecentDays(), request.getBaseDate(), request.getLimit(), now); - return themeRepository.findFamous(condition); + Pageable pageable = PageRequest.of(0, condition.getLimit().intValue()); + return themeRepository.findFamous(condition.startDate(), condition.endDate(), pageable); } @Transactional public void delete(long themeId) { - if (!themeJpaRepository.existsById(themeId)) { - throw new RoomEscapeException(ErrorCode.THEME_NOT_FOUND); - } + Theme theme = themeRepository.findById(themeId) + .orElseThrow(() -> new RoomEscapeException(ErrorCode.THEME_NOT_FOUND)); - if (reservationRepository.existsByThemeId(themeId)) { + slotRepository.findByTheme(theme).ifPresent(slot -> { throw new RoomEscapeException(ErrorCode.THEME_IN_USE); - } + }); - themeJpaRepository.deleteById(themeId); + themeRepository.deleteById(themeId); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b8e5508f13..293318f743 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,5 +8,4 @@ spring.jpa.properties.hibernate.format_sql=true spring.jpa.ddl-auto=create-drop spring.jpa.defer-datasource-initialization=true # OSIV 종료 -#spring.jpa.open-in-view=false -server.port=8081 \ No newline at end of file +spring.jpa.open-in-view=false \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index e02893ba92..5eb2847064 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,3 +1,6 @@ +-- 기준일: 2026-06-18 (오늘) +-- 인기 테마(최근 7일) 집계가 동작하도록 모든 슬롯을 2026-06-11 ~ 2026-06-17 구간에 배치 + -- 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'); @@ -18,80 +21,80 @@ INSERT INTO THEME (name, description, thumbnail_url) VALUES ('마법 학교', ' INSERT INTO THEME (name, description, thumbnail_url) VALUES ('고대 유적', '고대 문명의 유적을 탐험하세요', 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTbfoc4tfrkbUaKHBGhvdiTtoyzUmh3YNRsuw&s'); 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'); --- SLOT: 30개 (date + time_id + theme_id 고유 조합) +-- SLOT: 30개 (date + time_id + theme_id 고유 조합), 모두 최근 7일(2026-06-11 ~ 2026-06-17) 구간 -- Theme 1 (공포의 저택): slots 1~10 -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 1, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 2, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 3, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 4, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 5, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 6, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 7, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 8, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 9, 1); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 10, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-11', 1, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-11', 2, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-12', 3, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-12', 4, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-13', 5, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-13', 6, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-14', 7, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-15', 8, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-16', 9, 1); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-17', 10, 1); -- Theme 2 (우주 탐험): slots 11~18 -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-23', 3, 2); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 4, 2); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 5, 2); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 6, 2); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 7, 2); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 8, 2); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 9, 2); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 10, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-11', 3, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-12', 4, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-13', 5, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-14', 6, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-14', 7, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-15', 8, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-16', 9, 2); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-17', 10, 2); -- Theme 3 (마법 학교): slots 19~24 -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-24', 1, 3); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 2, 3); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 3, 3); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 4, 3); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 5, 3); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 6, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-12', 1, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-13', 2, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-14', 3, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-15', 4, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-16', 5, 3); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-17', 6, 3); -- Theme 4 (고대 유적): slots 25~28 -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-25', 7, 4); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 8, 4); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-27', 9, 4); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-28', 10, 4); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-13', 7, 4); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-14', 8, 4); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-15', 9, 4); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-16', 10, 4); -- Theme 5 (탐정 사무소): slots 29~30 -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-26', 11, 5); -INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-05-30', 1, 5); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-14', 11, 5); +INSERT INTO SLOT (date, time_id, theme_id) VALUES ('2026-06-17', 1, 5); -- RESERVATION: 33건 (slot_id 참조) -- Theme 1 (공포의 저택): 10건 → 1위 -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 1, 'APPROVED', '2026-05-21 09:12:33'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('이영희', 2, 'APPROVED', '2026-05-21 11:45:07'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('박민수', 3, 'APPROVED', '2026-05-22 14:30:51'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 4, 'APPROVED', '2026-05-22 18:05:22'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('정수진', 5, 'APPROVED', '2026-05-23 21:40:18'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('한동훈', 6, 'APPROVED', '2026-05-24 08:15:44'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('임채원', 7, 'APPROVED', '2026-05-24 10:50:09'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 8, 'APPROVED', '2026-05-25 13:22:37'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 9, 'APPROVED', '2026-05-26 16:48:55'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('유민호', 10, 'APPROVED', '2026-05-28 20:11:02'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 1, 'APPROVED', '2026-06-09 09:12:33'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('이영희', 2, 'APPROVED', '2026-06-09 11:45:07'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('박민수', 3, 'APPROVED', '2026-06-10 14:30:51'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 4, 'APPROVED', '2026-06-10 18:05:22'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('정수진', 5, 'APPROVED', '2026-06-11 21:40:18'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('한동훈', 6, 'APPROVED', '2026-06-11 08:15:44'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('임채원', 7, 'APPROVED', '2026-06-12 10:50:09'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 8, 'APPROVED', '2026-06-13 13:22:37'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 9, 'APPROVED', '2026-06-14 16:48:55'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('유민호', 10, 'APPROVED', '2026-06-15 20:11:02'); -- Theme 2 (우주 탐험): 8건 → 2위 -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('강민준', 11, 'APPROVED', '2026-05-20 07:33:19'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('조현아', 12, 'APPROVED', '2026-05-22 09:58:41'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 13, 'APPROVED', '2026-05-23 12:27:06'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 14, 'APPROVED', '2026-05-24 15:44:50'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('황준혁', 15, 'APPROVED', '2026-05-25 19:09:28'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('송미래', 16, 'APPROVED', '2026-05-26 08:41:13'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('안태양', 17, 'APPROVED', '2026-05-27 11:16:39'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('배소희', 18, 'APPROVED', '2026-05-29 14:52:04'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('강민준', 11, 'APPROVED', '2026-06-09 07:33:19'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('조현아', 12, 'APPROVED', '2026-06-10 09:58:41'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 13, 'APPROVED', '2026-06-11 12:27:06'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 14, 'APPROVED', '2026-06-12 15:44:50'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('황준혁', 15, 'APPROVED', '2026-06-12 19:09:28'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('송미래', 16, 'APPROVED', '2026-06-13 08:41:13'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('안태양', 17, 'APPROVED', '2026-06-14 11:16:39'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('배소희', 18, 'APPROVED', '2026-06-15 14:52:04'); -- Theme 3 (마법 학교): 6건 → 3위 -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('권지훈', 19, 'APPROVED', '2026-05-22 17:30:47'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 20, 'APPROVED', '2026-05-23 20:55:21'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 21, 'APPROVED', '2026-05-25 09:05:58'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('류지아', 22, 'APPROVED', '2026-05-26 12:38:16'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 23, 'APPROVED', '2026-05-27 15:11:33'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 24, 'APPROVED', '2026-05-29 18:47:09'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('권지훈', 19, 'APPROVED', '2026-06-10 17:30:47'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 20, 'APPROVED', '2026-06-11 20:55:21'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('김철수', 21, 'APPROVED', '2026-06-12 09:05:58'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('류지아', 22, 'APPROVED', '2026-06-13 12:38:16'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 23, 'APPROVED', '2026-06-14 15:11:33'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 24, 'APPROVED', '2026-06-15 18:47:09'); -- Theme 4 (고대 유적): 4건 → 4위 -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 25, 'APPROVED', '2026-05-23 08:23:42'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('전현무', 26, 'APPROVED', '2026-05-25 10:59:27'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 27, 'APPROVED', '2026-05-26 13:34:50'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('표민혁', 28, 'APPROVED', '2026-05-27 16:20:15'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 25, 'APPROVED', '2026-06-11 08:23:42'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('전현무', 26, 'APPROVED', '2026-06-12 10:59:27'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 27, 'APPROVED', '2026-06-13 13:34:50'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('표민혁', 28, 'APPROVED', '2026-06-14 16:20:15'); -- Theme 5 (탐정 사무소): 2건 → 5위 -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 29, 'APPROVED', '2026-05-24 19:48:33'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 30, 'APPROVED', '2026-05-28 09:14:06'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('서태양', 29, 'APPROVED', '2026-06-12 19:48:33'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('홍길동', 30, 'APPROVED', '2026-06-15 09:14:06'); -- 같은 슬롯 대기 예약 (slot 1 공유) -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자A', 1, 'WAITING', '2026-05-21 09:30:00'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자B', 1, 'WAITING', '2026-05-21 10:00:00'); -INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자C', 1, 'WAITING', '2026-05-21 10:30:00'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자A', 1, 'WAITING', '2026-06-09 09:30:00'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자B', 1, 'WAITING', '2026-06-09 10:00:00'); +INSERT INTO RESERVATION (name, slot_id, status, created_at) VALUES ('대기자C', 1, 'WAITING', '2026-06-09 10:30:00'); diff --git a/src/test/java/roomescape/MissionStep2Test.java b/src/test/java/roomescape/MissionStep2Test.java index de0a65207a..985f70bcfc 100644 --- a/src/test/java/roomescape/MissionStep2Test.java +++ b/src/test/java/roomescape/MissionStep2Test.java @@ -7,6 +7,7 @@ import java.sql.Connection; import java.sql.SQLException; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -20,6 +21,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; import roomescape.controller.dto.response.ReservationResponse; +import roomescape.domain.reservation.Status; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @@ -57,7 +59,8 @@ void init() { void DB_조회_API_전환() { jdbcTemplate.update("INSERT INTO slot (date, time_id, theme_id) VALUES (?, ?, ?)", "2023-08-05", 1, 1); - jdbcTemplate.update("INSERT INTO reservation (name, slot_id) VALUES (?, ?)", "브라운", 1); + jdbcTemplate.update("INSERT INTO reservation (name, slot_id, created_at, status) VALUES (?, ?, ?, ?)", "브라운", 1, + LocalTime.now(), Status.APPROVED.name()); List reservations = RestAssured.given().log().all() .when().get("/reservations") diff --git a/src/test/java/roomescape/RoomEscapeFixture.java b/src/test/java/roomescape/RoomEscapeFixture.java index f4011e11f1..3af1eaf064 100644 --- a/src/test/java/roomescape/RoomEscapeFixture.java +++ b/src/test/java/roomescape/RoomEscapeFixture.java @@ -5,7 +5,6 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; -import roomescape.controller.dto.request.ReservationCreateRequest; import roomescape.controller.dto.request.ReservationUpdateRequest; import roomescape.controller.dto.request.ThemeFamousFindRequest; import roomescape.domain.reservation.Reservation; @@ -48,16 +47,12 @@ public static ThemeFamousFindRequest themeFamousFindRequest() { return new ThemeFamousFindRequest(7L, FUTURE_DATE.getValue(), 10L); } - public static ReservationCreateRequest reservationCreateRequestWithName(ReservationName name) { - return new ReservationCreateRequest(name.getValue(), FUTURE_DATE.getValue(), 1L, 1L); - } - public static ReservationUpdateRequest reservationUpdateRequest() { return new ReservationUpdateRequest(NAME.getValue(), FUTURE_DATE.getValue(), 1L, 1L); } public static class SlotBuilder { - private long id = 1L; + private Long id = 1L; private ReservationDate date = FUTURE_DATE; private ReservationTime time = TIME; private Theme theme = THEME; @@ -67,6 +62,11 @@ public SlotBuilder id(long id) { return this; } + public SlotBuilder asNew() { + this.id = null; + return this; + } + public SlotBuilder date(ReservationDate date) { this.date = date; return this; @@ -88,7 +88,7 @@ public Slot build() { } public static class ReservationBuilder { - private long id = 1L; + private Long id = 1L; private ReservationName name = NAME; private Slot slot = RoomEscapeFixture.slot().build(); private Status status = Status.APPROVED; @@ -99,6 +99,11 @@ public ReservationBuilder id(long id) { return this; } + public ReservationBuilder asNew() { + this.id = null; + return this; + } + public ReservationBuilder name(String name) { this.name = new ReservationName(name); return this; diff --git a/src/test/java/roomescape/RoomescapeApplicationTest.java b/src/test/java/roomescape/RoomescapeApplicationTest.java index 951011a32e..a4949808ee 100644 --- a/src/test/java/roomescape/RoomescapeApplicationTest.java +++ b/src/test/java/roomescape/RoomescapeApplicationTest.java @@ -5,13 +5,8 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; -import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -20,10 +15,6 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.annotation.DirtiesContext; -import roomescape.controller.dto.request.ReservationCreateRequest; -import roomescape.domain.reservation.RankedReservation; -import roomescape.domain.reservation.ReservationName; -import roomescape.domain.reservation.Status; import roomescape.service.ReservationService; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -283,49 +274,6 @@ private int availableCount(String date, long themeId) { reserve("zeze", "2099-06-01", 1L, 999L, 404); } - - @Test - void 동시에_10명이_첫_예약_요청시_1명만_승인상태가_된다() throws Exception { - // 한 슬롯에 Approve된 예약은 반드시 1건 이하여야 한다. - int threads = 10; - var ready = new CountDownLatch(threads); - var start = new CountDownLatch(1); - var done = new CountDownLatch(threads); - var approved = new AtomicInteger(); - var waiting = new AtomicInteger(); - - var pool = Executors.newFixedThreadPool(threads); - - for (int i = 0; i < threads; i++) { - ReservationCreateRequest request = RoomEscapeFixture.reservationCreateRequestWithName( - new ReservationName(i + "")); - pool.submit(() -> { - ready.countDown(); - try { - start.await(); - RankedReservation result = reservationService.reserve(request, LocalDateTime.now()); - - if (result.getReservation().getStatus() == Status.APPROVED) { - approved.incrementAndGet(); - } - if (result.getReservation().getStatus() == Status.WAITING) { - waiting.incrementAndGet(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - done.countDown(); - } - }); - } - ready.await(); - start.countDown(); - done.await(); - - AssertionsForClassTypes.assertThat(approved.get()).isEqualTo(1); - AssertionsForClassTypes.assertThat(waiting.get()).isEqualTo(9); - } - private int reserveAndGetId(String name, String date, Long timeId, Long themeId) { Map params = new HashMap<>(); params.put("name", name); diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 29f909bc11..337150ed53 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import common.exception.RoomEscapeException; import java.time.LocalDateTime; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -48,21 +47,4 @@ static Stream nullCases() { Assertions.assertThat(id1WithSameDate.isEarlierThan(id2WithSameDate)).isFalse(); } - - @Test - void 예약_일정이_제공된_시점보다_과거라면_예외가_발생한다() { - Slot slot = RoomEscapeFixture.slot().date(RoomEscapeFixture.PAST_DATE).build(); - Reservation past = RoomEscapeFixture.reservation().slot(slot).build(); - - Assertions.assertThatThrownBy(() -> past.ensureNotPast(LocalDateTime.now())) - .isInstanceOf(RoomEscapeException.class); - } - - @Test - void 예약_일정이_제공된_시점보다_미래라면_예외가_발생하지_않는다() { - Slot slot = RoomEscapeFixture.slot().date(RoomEscapeFixture.FUTURE_DATE).build(); - Reservation future = RoomEscapeFixture.reservation().slot(slot).build(); - - Assertions.assertThatCode(() -> future.ensureNotPast(LocalDateTime.now())); - } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationsTest.java b/src/test/java/roomescape/domain/reservation/ReservationsTest.java index 73bae2957e..89be7d23ed 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationsTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationsTest.java @@ -77,7 +77,7 @@ class ReservationsTest { .id(2L).name("달수").status(Status.WAITING).createdAt(LocalDateTime.of(2099, 1, 1, 9, 1)).build(); Reservations rankedReservations = new Reservations(List.of(first, second)); - List results = rankedReservations.allRankedReservationsOf(); + List results = rankedReservations.rankedReservationsOf(); assertThat(results.get(0).getRank().getValue()).isEqualTo(0); assertThat(results.get(1).getRank().getValue()).isEqualTo(1); diff --git a/src/test/java/roomescape/service/ReservationServiceTest.java b/src/test/java/roomescape/service/ReservationServiceTest.java index c9345ad117..31533a896d 100644 --- a/src/test/java/roomescape/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/service/ReservationServiceTest.java @@ -24,9 +24,9 @@ import roomescape.domain.reservation.Status; import roomescape.domain.theme.Theme; import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeJpaRepository; +import roomescape.repository.ReservationTimeRepository; import roomescape.repository.SlotRepository; -import roomescape.repository.ThemeJpaRepository; +import roomescape.repository.ThemeRepository; @SpringBootTest(webEnvironment = WebEnvironment.NONE) @Transactional @@ -37,9 +37,9 @@ public class ReservationServiceTest { @Autowired private ReservationRepository reservationRepository; @Autowired - private ThemeJpaRepository themeRepository; + private ThemeRepository themeRepository; @Autowired - private ReservationTimeJpaRepository reservationTimeRepository; + private ReservationTimeRepository reservationTimeRepository; @Autowired private SlotRepository slotRepository; @@ -265,13 +265,14 @@ void promote_first_waiting_when_approved_moved_out() { private Reservation saveReservation(Slot slot, String name, Status status) { return reservationRepository.save( - RoomEscapeFixture.reservation().name(name).slot(slot).status(status).build()); + RoomEscapeFixture.reservation().name(name).slot(slot).status(status).asNew().build()); } private Slot saveSlot(Theme theme, ReservationTime time, ReservationDate date) { - Theme targetTheme = themeRepository.save(theme); - ReservationTime targetTime = reservationTimeRepository.save(time); + Theme targetTheme = themeRepository.save( + Theme.create(theme.getName(), theme.getDescription(), theme.getThumbnailUrl())); + ReservationTime targetTime = reservationTimeRepository.save(ReservationTime.of(time.getStartAt())); return slotRepository.save( - RoomEscapeFixture.slot().time(targetTime).theme(targetTheme).date(date).build()); + RoomEscapeFixture.slot().time(targetTime).theme(targetTheme).date(date).asNew().build()); } } diff --git a/src/test/java/roomescape/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/service/ReservationTimeServiceTest.java index 773a949aaa..e33526be5e 100644 --- a/src/test/java/roomescape/service/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/service/ReservationTimeServiceTest.java @@ -4,37 +4,42 @@ import common.exception.RoomEscapeException; import java.time.LocalDate; +import java.util.Optional; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import roomescape.RoomEscapeFixture; import roomescape.controller.dto.request.AvailableTimeFindRequest; -import roomescape.repository.ReservationRepository; -import roomescape.repository.ReservationTimeJpaRepository; +import roomescape.domain.reservation.ReservationTime; +import roomescape.domain.reservation.Slot; +import roomescape.repository.ReservationTimeRepository; +import roomescape.repository.SlotRepository; @ExtendWith(MockitoExtension.class) class ReservationTimeServiceTest { @Mock - private ReservationTimeJpaRepository reservationTimeRepository; + private ReservationTimeRepository reservationTimeRepository; @Mock - private ReservationRepository reservationRepository; + private SlotRepository slotRepository; @InjectMocks private ReservationTimeService reservationTimeService; @Test void 정상적인_시간_삭제는_성공해야_한다() { - given(reservationTimeRepository.existsById(1L)).willReturn(true); - given(reservationRepository.existsByTimeId(1L)).willReturn(false); + ReservationTime time = RoomEscapeFixture.TIME; + given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(time)); + given(slotRepository.findByTime(time)).willReturn(Optional.empty()); Assertions.assertThatNoException().isThrownBy(() -> reservationTimeService.delete(1L)); } @Test void 존재하지_않는_시간_삭제시_예외가_발생한다() { - given(reservationTimeRepository.existsById(999L)).willReturn(false); + given(reservationTimeRepository.findById(999L)).willReturn(Optional.empty()); Assertions.assertThatThrownBy(() -> reservationTimeService.delete(999L)) .isInstanceOf(RoomEscapeException.class); @@ -42,8 +47,10 @@ class ReservationTimeServiceTest { @Test void 예약이_있는_시간_삭제시_예외가_발생한다() { - given(reservationTimeRepository.existsById(1L)).willReturn(true); - given(reservationRepository.existsByTimeId(1L)).willReturn(true); + ReservationTime time = RoomEscapeFixture.TIME; + Slot slot = RoomEscapeFixture.slot().build(); + given(reservationTimeRepository.findById(1L)).willReturn(Optional.of(time)); + given(slotRepository.findByTime(time)).willReturn(Optional.of(slot)); Assertions.assertThatThrownBy(() -> reservationTimeService.delete(1L)) .isInstanceOf(RoomEscapeException.class); diff --git a/src/test/java/roomescape/service/ThemeServiceTest.java b/src/test/java/roomescape/service/ThemeServiceTest.java index 0b3fe8abbe..bc7aa0224c 100644 --- a/src/test/java/roomescape/service/ThemeServiceTest.java +++ b/src/test/java/roomescape/service/ThemeServiceTest.java @@ -15,20 +15,19 @@ import org.mockito.junit.jupiter.MockitoExtension; import roomescape.RoomEscapeFixture; import roomescape.controller.dto.request.ThemeFamousFindRequest; -import roomescape.repository.ReservationRepository; -import roomescape.repository.ThemeJpaRepository; +import roomescape.domain.reservation.Slot; +import roomescape.domain.theme.Theme; +import roomescape.repository.SlotRepository; import roomescape.repository.ThemeRepository; @ExtendWith(MockitoExtension.class) class ThemeServiceTest { - @Mock - private ThemeJpaRepository themeJpaRepository; @Mock private ThemeRepository themeRepository; @Mock - private ReservationRepository reservationRepository; + private SlotRepository slotRepository; @InjectMocks private ThemeService themeService; @@ -36,7 +35,7 @@ class ThemeServiceTest { @Test void 존재하지_않는_테마_조회_시_예외가_발생한다() { // given - given(themeJpaRepository.findById(999L)).willReturn(Optional.empty()); + given(themeRepository.findById(999L)).willReturn(Optional.empty()); // when & then Assertions.assertThatThrownBy(() -> themeService.find(999L)).isInstanceOf(RoomEscapeException.class); @@ -45,7 +44,7 @@ class ThemeServiceTest { @Test void 존재하지_않는_테마_삭제_시_예외가_발생한다() { // given - given(themeJpaRepository.existsById(999L)).willReturn(false); + given(themeRepository.findById(999L)).willReturn(Optional.empty()); // when & then Assertions.assertThatThrownBy(() -> themeService.delete(999L)).isInstanceOf(RoomEscapeException.class); @@ -60,13 +59,16 @@ class ThemeServiceTest { themeService.findFamous(request, LocalDate.now()); // then - verify(themeRepository).findFamous(any()); + verify(themeRepository).findFamous(any(), any(), any()); } @Test - void 삭제시_테마를_사용하는_예외가_있으면_예외가_발생한다() { - given(themeJpaRepository.existsById(1L)).willReturn(true); - given(reservationRepository.existsByThemeId(1L)).willReturn(true); + void 삭제시_테마를_사용하는_예약이_있으면_예외가_발생한다() { + Theme theme = RoomEscapeFixture.theme(); + Slot slot = RoomEscapeFixture.slot().build(); + given(themeRepository.findById(1L)).willReturn(Optional.of(theme)); + given(slotRepository.findByTheme(theme)).willReturn(Optional.of(slot)); + Assertions.assertThatThrownBy(() -> themeService.delete(1L)).isInstanceOf(RoomEscapeException.class); } }