From e92c7f662b23b63f05fd6ccc1c5b93a9f5602c50 Mon Sep 17 00:00:00 2001 From: rin Date: Sat, 6 Jun 2026 12:01:43 +0900 Subject: [PATCH 01/11] =?UTF-8?q?docs:=20=EC=82=AC=EC=9D=B4=ED=81=B42=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3794103e83..ca8f9ebb7e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ - [x] 이미 다른 사용자에 의해 예약된 슬롯(날짜+시간+테마)에 **대기를 신청**할 수 있다. - [x] 같은 사용자가 같은 슬롯에 **중복 대기할 수 없다**. +- [x] 예약 취소 시 같은 슬롯의 대기 1순위가 **자동으로 예약으로 승격**된다. +- [ ] 예약 수정 시 기존 슬롯의 대기 1순위가 **자동으로 예약으로 승격**된다. # Repository ## WaitingReservationRepository @@ -19,10 +21,19 @@ - 이유: 상태까지 하면 한 사용자가 예약 완료, 예약 대기 둘 다 가능하기 때문 - [x] 대기 취소를 하면 정상 삭제한다. - [x] 사용자 이름으로 예약 대기 목록 리스트를 가져온다. - + # 통합테스트 -## ReservationCancellationIntegration +## ReservationCancellationIntegrationTest - [x] 사용자가 본인의 예약을 취소하면 같은 슬롯의 1순위 대기가 예약으로 변경된다. +- [ ] 예약 취소 중 1순위 예약 대기 추가가 실패하면 전체가 롤백된다. +- [ ] 예약 취소 중 1순위 예약 대기 삭제가 실패하면 전체가 롤백된다. +- [ ] 예약 취소 중 기존 예약 삭제가 실패하면 전체가 롤백된다. + +## ReservationUpdateIntegrationTest +- [ ] 사용자가 본인의 예약을 수정하면 기존 슬롯의 1순위 대기가 예약으로 변경된다. +- [ ] 예약 수정 중 1순위 예약 대기 추가가 실패하면 전체가 롤백된다. +- [ ] 예약 수정 중 1순위 예약 대기 삭제가 실패하면 전체가 롤백된다. +- [ ] 예약 수정 중 예약 수정이 실패하면 전체가 롤백된다. # API @@ -64,12 +75,15 @@ - [x] 예약 가능한 시간에 대기 신청: 409 Conflict - [x] 존재하지 않는 date/time/theme: 404 Not Found - [x] 요청 값 누락/형식 오류: 400 Bad Request + - [x] 예약 대기 취소 **`DELETE /waiting-reservations/{id}`** - 설명: 사용자 본인 예약 대기 취소 - 응답 `204 No Content` + - 에러 처리 + - [ ] 대기가 이미 예약으로 전환된 경우: 409 Conflict - [x] 예약 대기 목록 조회 @@ -97,7 +111,7 @@ "rank" : 1, "createdAt": "2026-05-26T11:00:55" }, - { + { "id": 2, "name": "고래", "date": "2026-06-05", @@ -120,3 +134,18 @@ - 에러 처리 - [x] name이 비어있는 경우: 400 Bad Request + +# 동시성 처리 - Slot 테이블 도입 + +현재 동시성 문제 상황: +- 예약 생성 동시 요청: UNIQUE 제약으로 정합성은 보장되나 500 에러 발생 +- 예약 취소 + 예약 생성 동시 요청: 취소 후 대기 승격 시 UNIQUE 에러 및 트랜잭션 롤백 가능성 +- 예약 취소 + 대기 취소 동시 요청: 대기가 예약으로 승격된 후 취소 시도 시 의미 없는 204 반환 +- 예약 수정 + 예약 생성 동시 요청: 기존 슬롯 대기 승격 중 새 예약 생성 시 UNIQUE 에러 및 트랜잭션 롤백 가능성 +- 예약 수정 + 대기 취소 동시 요청: 수정으로 기존 슬롯 대기가 승격된 후 해당 대기 취소 시도 시 의미 없는 204 반환 + +해결 방향: +- `slot(id, date_id, time_id, theme_id)` 테이블 추가 +- `reservation`, `waiting_reservation`이 `slot_id`를 참조하도록 스키마 변경 +- 예약 생성/취소/수정/대기 신청/대기 취소 시 해당 slot row에 `SELECT FOR UPDATE`로 비관적 락 적용 +- 대기 취소 시 이미 예약으로 전환된 경우 409 응답 처리 From 84d605139f7650f86db6493bceb7cf3311137dc1 Mon Sep 17 00:00:00 2001 From: rin Date: Sat, 6 Jun 2026 12:13:15 +0900 Subject: [PATCH 02/11] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EC=8B=9C=20=EB=8C=80=EA=B8=B0=20=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EB=A1=A4=EB=B0=B1=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 예약 취소에서 기존 예약 삭제 로직을 대기 승격 조회 전에 하는 것으로 변경 --- README.md | 6 +- .../reservation/ReservationService.java | 33 ++---- ...eservationCancellationIntegrationTest.java | 103 +++++++++++++++++- 3 files changed, 116 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index ca8f9ebb7e..40bb6bac15 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ # 통합테스트 ## ReservationCancellationIntegrationTest - [x] 사용자가 본인의 예약을 취소하면 같은 슬롯의 1순위 대기가 예약으로 변경된다. -- [ ] 예약 취소 중 1순위 예약 대기 추가가 실패하면 전체가 롤백된다. -- [ ] 예약 취소 중 1순위 예약 대기 삭제가 실패하면 전체가 롤백된다. -- [ ] 예약 취소 중 기존 예약 삭제가 실패하면 전체가 롤백된다. +- [x] 예약 취소 중 1순위 예약 대기 추가가 실패하면 전체가 롤백된다. +- [x] 예약 취소 중 1순위 예약 대기 삭제가 실패하면 전체가 롤백된다. +- [x] 예약 취소 중 기존 예약 삭제가 실패하면 전체가 롤백된다. ## ReservationUpdateIntegrationTest - [ ] 사용자가 본인의 예약을 수정하면 기존 슬롯의 1순위 대기가 예약으로 변경된다. diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 5ac5daf0f7..19a88b99bc 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -1,12 +1,12 @@ package roomescape.domain.reservation; import jakarta.validation.Valid; -import java.time.LocalDate; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.domain.reservation.dto.ReservationCreationRequest; import roomescape.domain.reservation.dto.ReservationCreationResponse; import roomescape.domain.reservation.dto.ReservationResponse; @@ -42,14 +42,12 @@ public ReservationCreationResponse createReservation(ReservationCreationRequest Theme theme = themeService.findById(request.themeId()); validateNotDuplicated(request.dateId(), request.timeId(), request.themeId()); Reservation savedReservation = reservationRepository.save( - request.toEntity(reservationDate, reservationTime, theme)); + request.toEntity(reservationDate, reservationTime, theme)); return ReservationCreationResponse.from(savedReservation); } public List getAllReservations() { - return reservationRepository.findAll().stream() - .map(ReservationResponse::from) - .toList(); + return reservationRepository.findAll().stream().map(ReservationResponse::from).toList(); } public void deleteReservation(Long id) { @@ -60,31 +58,24 @@ public void deleteReservation(Long id) { } public List getReservationsByName(String name) { - return reservationRepository.findByName(name).stream() - .map(ReservationResponse::from) - .toList(); + return reservationRepository.findByName(name).stream().map(ReservationResponse::from).toList(); } + @Transactional public void cancelReservation(Long id) { Reservation reservation = findById(id); validateNotToday(reservation.getDate()); + reservationRepository.deleteById(id); Optional waitingReservationOpt = waitingReservationRepository.findOldestBySlot( - reservation.getDate().getId(), - reservation.getTime().getId(), - reservation.getTheme().getId() - ); + reservation.getDate().getId(), reservation.getTime().getId(), reservation.getTheme().getId()); if (waitingReservationOpt.isPresent()) { WaitingReservation waitingReservation = waitingReservationOpt.get(); - reservationRepository.save(Reservation.createWithoutId( - waitingReservation.getName(), - waitingReservation.getDate(), - waitingReservation.getTime(), - waitingReservation.getTheme() - )); + reservationRepository.save( + Reservation.createWithoutId(waitingReservation.getName(), waitingReservation.getDate(), + waitingReservation.getTime(), waitingReservation.getTheme())); waitingReservationRepository.deleteById(waitingReservation.getId()); } - reservationRepository.deleteById(id); } public ReservationResponse updateReservation(Long id, @Valid ReservationUpdateRequest request) { @@ -106,7 +97,7 @@ public ReservationResponse updateReservation(Long id, @Valid ReservationUpdateRe private Reservation findById(Long id) { return reservationRepository.findById(id) - .orElseThrow(() -> new RoomescapeException(ReservationErrorCode.RESERVATION_NOT_FOUND)); + .orElseThrow(() -> new RoomescapeException(ReservationErrorCode.RESERVATION_NOT_FOUND)); } private void validateNotDuplicated(Long dateId, Long timeId, Long themeId) { @@ -116,7 +107,7 @@ private void validateNotDuplicated(Long dateId, Long timeId, Long themeId) { } private void validateNotPast(ReservationDate reservationDate, ReservationTime reservationTime) { - if(reservationDate.isPast(reservationTime)) { + if (reservationDate.isPast(reservationTime)) { throw new RoomescapeException(ReservationDateErrorCode.PAST_DATE_NOT_ALLOWED); } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationCancellationIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationCancellationIntegrationTest.java index 68190c3f70..1e6f18a875 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationCancellationIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationCancellationIntegrationTest.java @@ -1,6 +1,9 @@ package roomescape.domain.reservation; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import java.time.LocalDate; import java.time.LocalDateTime; @@ -9,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.jdbc.Sql; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; @@ -26,10 +30,10 @@ class ReservationCancellationIntegrationTest { @Autowired private ReservationService reservationService; - @Autowired + @MockitoSpyBean private ReservationRepository reservationRepository; - @Autowired + @MockitoSpyBean private WaitingReservationRepository waitingReservationRepository; @Autowired @@ -79,6 +83,101 @@ void setUp() { assertThat(waitingReservationRepository.findById(otherSlotOldest.getId())).isPresent(); } + @Test + void 예약_취소_중_1순위_예약_대기_추가가_실패하면_전체가_롤백된다() { + Reservation cancelledReservation = reservationRepository.save( + Reservation.createWithoutId( + "테스터", + cancelledSlot.date(), + cancelledSlot.time(), + cancelledSlot.theme() + ) + ); + Slot otherSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(11, 0), "스릴러"); + waitingReservationRepository.save( + waiting("다른슬롯", otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) + ); + WaitingReservation firstWaiting = waitingReservationRepository.save( + waiting("이산", cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + ); + waitingReservationRepository.save( + waiting("고래", cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + ); + + doThrow(new RuntimeException()) + .when(reservationRepository) + .save(any(Reservation.class)); + + assertThatThrownBy(() -> reservationService.cancelReservation(cancelledReservation.getId())) + .isInstanceOf(RuntimeException.class); + + assertThat(reservationRepository.findById(cancelledReservation.getId())).isPresent(); + assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); + assertThat(reservationRepository.findByName("이산")).isEmpty(); + } + + @Test + void 예약_취소_중_1순위_예약_대기_삭제가_실패하면_전체가_롤백된다() { + Reservation cancelledReservation = reservationRepository.save( + Reservation.createWithoutId( + "테스터", + cancelledSlot.date(), + cancelledSlot.time(), + cancelledSlot.theme() + ) + ); + Slot otherSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(11, 0), "스릴러"); + waitingReservationRepository.save( + waiting("다른슬롯", otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) + ); + WaitingReservation firstWaiting = waitingReservationRepository.save( + waiting("이산", cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + ); + waitingReservationRepository.save( + waiting("고래", cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + ); + + doThrow(new RuntimeException()).when(waitingReservationRepository).deleteById(firstWaiting.getId()); + + assertThatThrownBy(() -> reservationService.cancelReservation(cancelledReservation.getId())) + .isInstanceOf(RuntimeException.class); + + assertThat(reservationRepository.findById(cancelledReservation.getId())).isPresent(); + assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); + assertThat(reservationRepository.findByName("이산")).isEmpty(); + } + + @Test + void 예약_취소_중_기존_예약_삭제가_실패하면_전체가_롤백된다() { + Reservation cancelledReservation = reservationRepository.save( + Reservation.createWithoutId( + "테스터", + cancelledSlot.date(), + cancelledSlot.time(), + cancelledSlot.theme() + ) + ); + Slot otherSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(11, 0), "스릴러"); + waitingReservationRepository.save( + waiting("다른슬롯", otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) + ); + WaitingReservation firstWaiting = waitingReservationRepository.save( + waiting("이산", cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + ); + waitingReservationRepository.save( + waiting("고래", cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + ); + + doThrow(new RuntimeException()).when(reservationRepository).deleteById(cancelledReservation.getId()); + + assertThatThrownBy(() -> reservationService.cancelReservation(cancelledReservation.getId())) + .isInstanceOf(RuntimeException.class); + + assertThat(reservationRepository.findById(cancelledReservation.getId())).isPresent(); + assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); + assertThat(reservationRepository.findByName("이산")).isEmpty(); + } + private WaitingReservation waiting(String name, Slot slot, LocalDateTime createdAt) { return WaitingReservation.createWithoutId(name, slot.date(), slot.time(), slot.theme(), createdAt); } From 6cb6416ee598677743362099a6e3167cd10d0fcb Mon Sep 17 00:00:00 2001 From: rin Date: Sat, 6 Jun 2026 12:39:39 +0900 Subject: [PATCH 03/11] =?UTF-8?q?fix:=20=EA=B3=BC=EA=B1=B0=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EC=B7=A8=EC=86=8C=20=EB=B0=8F=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EB=B6=88=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리팩토링 진행하다가 빠진 부분 다시 추가 --- .../domain/reservation/ReservationService.java | 9 +++++++-- .../support/exception/ReservationDateErrorCode.java | 2 +- .../WaitingReservationServiceTest.java | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 19a88b99bc..2247d74f3d 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -64,7 +64,7 @@ public List getReservationsByName(String name) { @Transactional public void cancelReservation(Long id) { Reservation reservation = findById(id); - validateNotToday(reservation.getDate()); + validateModifiable(reservation.getDate(), reservation.getTime()); reservationRepository.deleteById(id); Optional waitingReservationOpt = waitingReservationRepository.findOldestBySlot( @@ -80,7 +80,7 @@ public void cancelReservation(Long id) { public ReservationResponse updateReservation(Long id, @Valid ReservationUpdateRequest request) { Reservation reservation = findById(id); - validateNotToday(reservation.getDate()); + validateModifiable(reservation.getDate(), reservation.getTime()); ReservationDate newReservationDate = reservationDateService.findById(request.dateId()); ReservationTime newReservationTime = reservationTimeService.findById(request.timeId()); @@ -117,4 +117,9 @@ private void validateNotToday(ReservationDate reservationDate) { throw new RoomescapeException(ReservationDateErrorCode.TODAY_NOT_MODIFIED); } } + + private void validateModifiable(ReservationDate reservationDate, ReservationTime reservationTime) { + validateNotPast(reservationDate, reservationTime); + validateNotToday(reservationDate); + } } diff --git a/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java b/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java index 2a11ac48ca..401dbeddaf 100644 --- a/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java @@ -16,7 +16,7 @@ public enum ReservationDateErrorCode implements ErrorCode { TODAY_NOT_MODIFIED(HttpStatus.BAD_REQUEST, "당일 예약은 수정 및 취소가 불가능합니다.", "예약일이 오늘 이후인 예약만 변경할 수 있습니다."), PAST_DATE_NOT_ALLOWED(HttpStatus.BAD_REQUEST, - "과거 시점의 데이터를 등록할 수 없습니다.", "현재 시스템 날짜 및 시각과 요청 날짜 및 시각 데이터를 확인하십시오."), + "과거 예약은 수정 및 취소가 불가능합니다.", "현재 시스템 날짜 및 시각과 요청 날짜 및 시각 데이터를 확인하십시오."), ; private final HttpStatus httpStatus; diff --git a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java index 64153add01..40af34a469 100644 --- a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java +++ b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java @@ -130,7 +130,7 @@ void setUp() { assertThatThrownBy(() -> waitingReservationService.createWaitingReservation(request)) .isInstanceOf(RoomescapeException.class) - .hasMessageContaining("과거 시점의 데이터를 등록할 수 없습니다."); + .hasMessageContaining("과거 예약은 수정 및 취소가 불가능합니다."); } @Test From f78413fbe7eab30e9785118dc55041797190a536 Mon Sep 17 00:00:00 2001 From: rin Date: Sat, 6 Jun 2026 14:36:35 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=8B=9C=20=EB=8C=80=EA=B8=B0=20=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EB=A1=A4=EB=B0=B1=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +- .../reservation/ReservationService.java | 24 ++- .../ReservationUpdatingIntegrationTest.java | 165 ++++++++++++++++++ 3 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java diff --git a/README.md b/README.md index 40bb6bac15..1e0f1a6c58 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - [x] 이미 다른 사용자에 의해 예약된 슬롯(날짜+시간+테마)에 **대기를 신청**할 수 있다. - [x] 같은 사용자가 같은 슬롯에 **중복 대기할 수 없다**. - [x] 예약 취소 시 같은 슬롯의 대기 1순위가 **자동으로 예약으로 승격**된다. -- [ ] 예약 수정 시 기존 슬롯의 대기 1순위가 **자동으로 예약으로 승격**된다. +- [x] 예약 수정 시 기존 슬롯의 대기 1순위가 **자동으로 예약으로 승격**된다. # Repository ## WaitingReservationRepository @@ -30,10 +30,10 @@ - [x] 예약 취소 중 기존 예약 삭제가 실패하면 전체가 롤백된다. ## ReservationUpdateIntegrationTest -- [ ] 사용자가 본인의 예약을 수정하면 기존 슬롯의 1순위 대기가 예약으로 변경된다. -- [ ] 예약 수정 중 1순위 예약 대기 추가가 실패하면 전체가 롤백된다. -- [ ] 예약 수정 중 1순위 예약 대기 삭제가 실패하면 전체가 롤백된다. -- [ ] 예약 수정 중 예약 수정이 실패하면 전체가 롤백된다. +- [x] 사용자가 본인의 예약을 수정하면 기존 슬롯의 1순위 대기가 예약으로 변경된다. +- [x] 예약 수정 중 1순위 예약 대기 추가가 실패하면 전체가 롤백된다. +- [x] 예약 수정 중 1순위 예약 대기 삭제가 실패하면 전체가 롤백된다. +- [x] 예약 수정 중 예약 수정이 실패하면 전체가 롤백된다. # API diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 2247d74f3d..3413c8ace2 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -67,17 +67,10 @@ public void cancelReservation(Long id) { validateModifiable(reservation.getDate(), reservation.getTime()); reservationRepository.deleteById(id); - Optional waitingReservationOpt = waitingReservationRepository.findOldestBySlot( - reservation.getDate().getId(), reservation.getTime().getId(), reservation.getTheme().getId()); - if (waitingReservationOpt.isPresent()) { - WaitingReservation waitingReservation = waitingReservationOpt.get(); - reservationRepository.save( - Reservation.createWithoutId(waitingReservation.getName(), waitingReservation.getDate(), - waitingReservation.getTime(), waitingReservation.getTheme())); - waitingReservationRepository.deleteById(waitingReservation.getId()); - } + promoteWaitingReservation(reservation); } + @Transactional public ReservationResponse updateReservation(Long id, @Valid ReservationUpdateRequest request) { Reservation reservation = findById(id); validateModifiable(reservation.getDate(), reservation.getTime()); @@ -92,6 +85,7 @@ public ReservationResponse updateReservation(Long id, @Valid ReservationUpdateRe if (updatedCount == 0) { log.warn(" 수정할 예약 건이 없습니다. reservationId={}", id); } + promoteWaitingReservation(reservation); return ReservationResponse.from(findById(id)); } @@ -122,4 +116,16 @@ private void validateModifiable(ReservationDate reservationDate, ReservationTime validateNotPast(reservationDate, reservationTime); validateNotToday(reservationDate); } + + private void promoteWaitingReservation(Reservation reservation) { + Optional waitingReservationOpt = waitingReservationRepository.findOldestBySlot( + reservation.getDate().getId(), reservation.getTime().getId(), reservation.getTheme().getId()); + if (waitingReservationOpt.isPresent()) { + WaitingReservation waitingReservation = waitingReservationOpt.get(); + reservationRepository.save( + Reservation.createWithoutId(waitingReservation.getName(), waitingReservation.getDate(), + waitingReservation.getTime(), waitingReservation.getTheme())); + waitingReservationRepository.deleteById(waitingReservation.getId()); + } + } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java new file mode 100644 index 0000000000..9039fc7f11 --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java @@ -0,0 +1,165 @@ +package roomescape.domain.reservation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +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.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.reservation.dto.ReservationResponse; +import roomescape.domain.reservation.dto.ReservationUpdateRequest; +import roomescape.domain.reservationdate.ReservationDate; +import roomescape.domain.reservationdate.ReservationDateRepository; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.reservationtime.ReservationTimeRepository; +import roomescape.domain.theme.Theme; +import roomescape.domain.theme.ThemeRepository; +import roomescape.domain.waitingreservation.WaitingReservation; +import roomescape.domain.waitingreservation.WaitingReservationRepository; + +@SpringBootTest +@Sql("/truncate.sql") +class ReservationUpdatingIntegrationTest { + + @Autowired + private ReservationService reservationService; + + @MockitoSpyBean + private ReservationRepository reservationRepository; + + @MockitoSpyBean + private WaitingReservationRepository waitingReservationRepository; + + @Autowired + private ReservationDateRepository reservationDateRepository; + + @Autowired + private ReservationTimeRepository reservationTimeRepository; + + @Autowired + private ThemeRepository themeRepository; + + private Slot originSlot; + private Slot updateSlot; + private Reservation originReservation; + + @BeforeEach + void setUp() { + originSlot = insertSlot(LocalDate.now().plusDays(2), LocalTime.of(10, 0), "공포"); + + originReservation = reservationRepository.save( + Reservation.createWithoutId( + "테스터", + originSlot.date(), + originSlot.time(), + originSlot.theme() + ) + ); + + updateSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(10, 0), "공포"); + } + + @Test + void 사용자가_본인의_예약을_수정하면_같은_슬롯의_1순위_대기가_예약으로_변경된다() { + ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); + + WaitingReservation firstWaiting = waitingReservationRepository.save( + waiting("이산", originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + ); + WaitingReservation secondWaiting = waitingReservationRepository.save( + waiting("고래", originSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + ); + + reservationService.updateReservation(originReservation.getId(), updateRequest); + + assertThat(reservationRepository.findByName("테스터")).hasSize(1); + assertThat(reservationRepository.findByName("이산")).hasSize(1); + assertThat(reservationRepository.existsByDateIdAndTimeIdAndThemeId(originSlot.date.getId(), originSlot.time.getId(), originSlot.theme.getId())).isTrue(); + assertThat(reservationRepository.existsByDateIdAndTimeIdAndThemeId(updateSlot.date.getId(), updateSlot.time.getId(), originSlot.theme.getId())).isTrue(); + assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isEmpty(); + assertThat(waitingReservationRepository.findById(secondWaiting.getId())).isPresent(); + } + + @Test + void 예약_수정_중_예약_수정이_실패하면_전체가_롤백된다() { + ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); + + WaitingReservation firstWaiting = waitingReservationRepository.save( + waiting("이산", originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + ); + + doThrow(new RuntimeException()).when(reservationRepository).updateReservation(originReservation.getId(), updateRequest.dateId(), updateRequest.timeId()); + + assertThatThrownBy(() -> reservationService.updateReservation(originReservation.getId(), updateRequest)) + .isInstanceOf(RuntimeException.class); + + assertThat(reservationRepository.findById(originReservation.getId())).isPresent(); + assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); + assertThat(reservationRepository.findByName("이산")).isEmpty(); + } + + @Test + void 예약_수정_중_1순위_예약_대기_추가가_실패하면_전체가_롤백된다() { + ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); + + WaitingReservation firstWaiting = waitingReservationRepository.save( + waiting("이산", originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + ); + + doThrow(new RuntimeException()) + .when(reservationRepository) + .save(any(Reservation.class)); + + assertThatThrownBy(() -> reservationService.updateReservation(originReservation.getId(), updateRequest)) + .isInstanceOf(RuntimeException.class); + + assertThat(reservationRepository.findById(originReservation.getId())).isPresent(); + assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); + assertThat(reservationRepository.findByName("이산")).isEmpty(); + } + + @Test + void 예약_수정_중_1순위_예약_대기_삭제가_실패하면_전체가_롤백된다() { + ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); + + WaitingReservation firstWaiting = waitingReservationRepository.save( + waiting("이산", originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + ); + + doThrow(new RuntimeException()).when(waitingReservationRepository).deleteById(firstWaiting.getId()); + + assertThatThrownBy(() -> reservationService.updateReservation(originReservation.getId(), updateRequest)) + .isInstanceOf(RuntimeException.class); + + assertThat(reservationRepository.findById(originReservation.getId())).isPresent(); + assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); + assertThat(reservationRepository.findByName("이산")).isEmpty(); + } + + private WaitingReservation waiting(String name, Slot slot, LocalDateTime createdAt) { + return WaitingReservation.createWithoutId(name, slot.date(), slot.time(), slot.theme(), createdAt); + } + + private Slot insertSlot(LocalDate playDay, LocalTime startAt, String themeName) { + ReservationDate date = reservationDateRepository.save(ReservationDate.createWithoutId(playDay)); + ReservationTime time = reservationTimeRepository.save(ReservationTime.createWithoutId(startAt)); + Theme theme = themeRepository.save(Theme.createWithoutId(themeName, "테마 내용", "/themes/" + themeName)); + return new Slot(date, time, theme); + } + + private record Slot( + ReservationDate date, + ReservationTime time, + Theme theme + ) { + + } +} From e553b31d7681a9e939cf3cb406861f5d9187e4c3 Mon Sep 17 00:00:00 2001 From: rin Date: Sat, 6 Jun 2026 15:24:16 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=EC=98=88=EC=95=BD=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0=20=EC=B7=A8=EC=86=8C=20=EC=8B=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=EC=9C=BC=EB=A1=9C=20=EC=8A=B9=EA=B2=A9?= =?UTF-8?q?=EB=90=9C=20=EA=B2=BD=EC=9A=B0=20409=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../JdbcReservationRepository.java | 14 ++++++ .../reservation/ReservationRepository.java | 2 + .../WaitingReservationService.java | 43 ++++++++++++------- .../WaitingReservationErrorCode.java | 7 ++- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1e0f1a6c58..41f171b1b5 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ - 설명: 사용자 본인 예약 대기 취소 - 응답 `204 No Content` - 에러 처리 - - [ ] 대기가 이미 예약으로 전환된 경우: 409 Conflict + - [x] 대기가 이미 예약으로 전환된 경우: 409 Conflict - [x] 예약 대기 목록 조회 diff --git a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java index bcca44d169..79d5189e23 100644 --- a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java @@ -104,6 +104,15 @@ select exists( ); """; + private static final String EXIST_BY_NAME_DATE_TIME_THEME_SQL = + """ + select exists( + select 1 + from reservation + where name = ? and date_id = ? and time_id = ? and theme_id = ? + ); + """; + private final JdbcTemplate jdbcTemplate; @Override @@ -191,6 +200,11 @@ public boolean existsByDateIdAndTimeIdAndThemeId(Long dateId, Long timeId, Long return jdbcTemplate.queryForObject(EXIST_BY_DATE_TIME_THEME_SQL, Boolean.class, dateId, timeId, themeId); } + @Override + public boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, Long dateId, Long timeId, Long themeId) { + return jdbcTemplate.queryForObject(EXIST_BY_NAME_DATE_TIME_THEME_SQL, Boolean.class, name, dateId, timeId, themeId); + } + private RowMapper reservationRowMapper() { return (rs, rowNum) -> Reservation.of( rs.getLong("id"), diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index 6909702f03..9c9c462a51 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -26,4 +26,6 @@ public interface ReservationRepository { int updateReservation(Long id, Long dateId, Long timeId); boolean existsByDateIdAndTimeIdAndThemeId(Long dateId, Long timeId, Long themeId); + + boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, Long dateId, Long timeId, Long themeId); } diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java index 0b2c1f2271..7c831158b2 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java @@ -43,8 +43,35 @@ public WaitingReservationCreationResponse createWaitingReservation(WaitingReserv return WaitingReservationCreationResponse.from(savedWaitingReservation); } + public void cancelWaitingReservation(Long id) { + + WaitingReservation waitingReservation = waitingReservationRepository + .findById(id) + .orElseThrow(() -> new RoomescapeException(WaitingReservationErrorCode.WAITING_RESERVATION_NOT_FOUND)); + + if (reservationRepository.existsByNameAndDateIdAndTimeIdAndThemeId( + waitingReservation.getName(), + waitingReservation.getDate().getId(), + waitingReservation.getTime().getId(), + waitingReservation.getTheme().getId())) { + throw new RoomescapeException(WaitingReservationErrorCode.ALREADY_PROMOTED_TO_RESERVATION); + } + + int deletedCount = waitingReservationRepository.deleteById(id); + if (deletedCount == 0) { + log.warn("이미 삭제된 예약 대기 삭제 요청이 들어왔습니다. reservationId={}", id); + } + } + + public List getWaitingReservationsWithRankByName(String name) { + return waitingReservationRepository.findAllByNameWithRank(name) + .stream() + .map(WaitingReservationWithRankResponse::from) + .toList(); + } + private void validateNotPast(ReservationDate reservationDate, ReservationTime reservationTime) { - if(reservationDate.isPast(reservationTime)) { + if (reservationDate.isPast(reservationTime)) { throw new RoomescapeException(ReservationDateErrorCode.PAST_DATE_NOT_ALLOWED); } } @@ -69,18 +96,4 @@ private void validateSlotIsReserved(WaitingReservationCreationRequest request) { throw new RoomescapeException(WaitingReservationErrorCode.AVAILABLE_SLOT_NOT_WAITABLE); } } - - public void cancelWaitingReservation(Long id) { - int deletedCount = waitingReservationRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("이미 삭제된 예약 대기 삭제 요청이 들어왔습니다. reservationId={}", id); - } - } - - public List getWaitingReservationsWithRankByName(String name) { - return waitingReservationRepository.findAllByNameWithRank(name) - .stream() - .map(WaitingReservationWithRankResponse::from) - .toList(); - } } diff --git a/src/main/java/roomescape/support/exception/WaitingReservationErrorCode.java b/src/main/java/roomescape/support/exception/WaitingReservationErrorCode.java index 34d8d3c70e..df187b8c72 100644 --- a/src/main/java/roomescape/support/exception/WaitingReservationErrorCode.java +++ b/src/main/java/roomescape/support/exception/WaitingReservationErrorCode.java @@ -14,7 +14,12 @@ public enum WaitingReservationErrorCode implements ErrorCode { PAST_TIME_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "과거 시간에는 예약 대기를 신청할 수 없습니다.", "예약 대기 날짜와 시간이 현재 이후인지 확인하십시오."), DUPLICATE_WAITING_RESERVATION(HttpStatus.CONFLICT, - "중복으로 대기 신청을 할 수 없습니다.", "동일한 이름으로 신청된 예약 대기가 있는지 확인하세요."); + "중복으로 대기 신청을 할 수 없습니다.", "동일한 이름으로 신청된 예약 대기가 있는지 확인하세요."), + ALREADY_PROMOTED_TO_RESERVATION(HttpStatus.CONFLICT, + "해당 예약 대기는 이미 예약으로 전환되었습니다.", "예약 목록을 확인하십시오."), + WAITING_RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, + "지정한 식별자에 해당하는 예약 대기 엔티티를 찾을 수 없습니다.", "요청한 예약 대기 ID의 유효성 및 DB 존재 여부를 확인하십시오."), + ; private final HttpStatus httpStatus; private final String message; From 709f06491b3a596a4f4e8173451d026b65d0a464 Mon Sep 17 00:00:00 2001 From: rin Date: Sat, 6 Jun 2026 21:28:36 +0900 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20=EC=98=88=EC=95=BD=EB=90=9C=20?= =?UTF-8?q?=EC=8A=AC=EB=A1=AF=EC=97=90=20=EB=8C=80=EA=B8=B0=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EC=8B=9C=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../waitingreservation/WaitingReservationService.java | 11 +++++++++++ .../exception/WaitingReservationErrorCode.java | 3 ++- src/main/resources/schema.sql | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java index 7c831158b2..6d280c46e5 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java @@ -36,6 +36,7 @@ public WaitingReservationCreationResponse createWaitingReservation(WaitingReserv Theme theme = themeService.findById(request.themeId()); validateNotPast(date, time); validateSlotIsReserved(request); + validateAlreadyReserved(request); validateDuplicationOfWaitingReservation(request); WaitingReservation waitingReservation = request.toEntity(date, time, theme, LocalDateTime.now()); @@ -76,6 +77,16 @@ private void validateNotPast(ReservationDate reservationDate, ReservationTime re } } + private void validateAlreadyReserved(WaitingReservationCreationRequest request) { + if(reservationRepository.existsByNameAndDateIdAndTimeIdAndThemeId( + request.name(), + request.dateId(), + request.timeId(), + request.themeId())) { + throw new RoomescapeException(WaitingReservationErrorCode.ALREADY_RESERVED); + } + } + private void validateDuplicationOfWaitingReservation(WaitingReservationCreationRequest request) { if (waitingReservationRepository.existsByNameAndDateIdAndTimeIdAndThemeId( request.name(), diff --git a/src/main/java/roomescape/support/exception/WaitingReservationErrorCode.java b/src/main/java/roomescape/support/exception/WaitingReservationErrorCode.java index df187b8c72..95ca5e3fa2 100644 --- a/src/main/java/roomescape/support/exception/WaitingReservationErrorCode.java +++ b/src/main/java/roomescape/support/exception/WaitingReservationErrorCode.java @@ -19,7 +19,8 @@ public enum WaitingReservationErrorCode implements ErrorCode { "해당 예약 대기는 이미 예약으로 전환되었습니다.", "예약 목록을 확인하십시오."), WAITING_RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "지정한 식별자에 해당하는 예약 대기 엔티티를 찾을 수 없습니다.", "요청한 예약 대기 ID의 유효성 및 DB 존재 여부를 확인하십시오."), - ; + ALREADY_RESERVED(HttpStatus.CONFLICT, + "이미 예약 완료된 것은 예약 대기가 불가능합니다.", "선택한 옵션으로 예약된 것이 있는지 확인하세요."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 5d5ee09367..63dfc0b15e 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS reservation time_id BIGINT NOT NULL, theme_id BIGINT NOT NULL, PRIMARY KEY (id), + UNIQUE(date_id, time_id, theme_id), FOREIGN KEY (time_id) REFERENCES reservation_time (id), FOREIGN KEY (date_id) REFERENCES reservation_date (id), FOREIGN KEY (theme_id) REFERENCES theme (id) From d0993d8299cefb1a365289fb2b61ec2a5d1aa3cb Mon Sep 17 00:00:00 2001 From: rin Date: Mon, 8 Jun 2026 12:44:18 +0900 Subject: [PATCH 07/11] =?UTF-8?q?style:=20=EC=8A=A4=ED=8A=B8=EB=A6=BC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=8F=AC=EB=A7=B7=ED=8C=85=20=EC=A4=84?= =?UTF-8?q?=EB=B0=94=EA=BF=88=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/ReservationService.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 3413c8ace2..6704d01079 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -47,7 +47,10 @@ public ReservationCreationResponse createReservation(ReservationCreationRequest } public List getAllReservations() { - return reservationRepository.findAll().stream().map(ReservationResponse::from).toList(); + return reservationRepository.findAll() + .stream() + .map(ReservationResponse::from) + .toList(); } public void deleteReservation(Long id) { @@ -58,7 +61,10 @@ public void deleteReservation(Long id) { } public List getReservationsByName(String name) { - return reservationRepository.findByName(name).stream().map(ReservationResponse::from).toList(); + return reservationRepository.findByName(name) + .stream() + .map(ReservationResponse::from) + .toList(); } @Transactional @@ -79,7 +85,8 @@ public ReservationResponse updateReservation(Long id, @Valid ReservationUpdateRe ReservationTime newReservationTime = reservationTimeService.findById(request.timeId()); validateNotPast(newReservationDate, newReservationTime); - validateNotDuplicated(request.dateId(), request.timeId(), reservation.getTheme().getId()); + validateNotDuplicated(request.dateId(), request.timeId(), reservation.getTheme() + .getId()); int updatedCount = reservationRepository.updateReservation(id, request.dateId(), request.timeId()); if (updatedCount == 0) { @@ -119,7 +126,10 @@ private void validateModifiable(ReservationDate reservationDate, ReservationTime private void promoteWaitingReservation(Reservation reservation) { Optional waitingReservationOpt = waitingReservationRepository.findOldestBySlot( - reservation.getDate().getId(), reservation.getTime().getId(), reservation.getTheme().getId()); + reservation.getDate() + .getId(), reservation.getTime() + .getId(), reservation.getTheme() + .getId()); if (waitingReservationOpt.isPresent()) { WaitingReservation waitingReservation = waitingReservationOpt.get(); reservationRepository.save( From b6759a006ab0ccadb5da42ac55054d57e5c55fb7 Mon Sep 17 00:00:00 2001 From: rin Date: Mon, 8 Jun 2026 15:37:00 +0900 Subject: [PATCH 08/11] =?UTF-8?q?fix:=20=EB=8C=80=EA=B8=B0=20=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9=20=EC=8B=9C=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=B9=84=EA=B4=80=EC=A0=81=20=EB=9D=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../waitingreservation/JdbcWaitingReservationRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepository.java b/src/main/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepository.java index 1e1913c523..132d734bb2 100644 --- a/src/main/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepository.java +++ b/src/main/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepository.java @@ -46,6 +46,7 @@ select exists( where wr.date_id = ? and wr.time_id = ? and wr.theme_id = ? order by wr.created_at asc, wr.id asc limit 1 + for update """; private static final String FIND_BY_NAME_SQL = """ From d20bfd0fda88a640c0b044d8e478006b54d9f9a1 Mon Sep 17 00:00:00 2001 From: rin Date: Thu, 18 Jun 2026 13:33:26 +0900 Subject: [PATCH 09/11] =?UTF-8?q?[1=EB=8B=A8=EA=B3=84]=20:=20JPA=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JdbcRepository를 JpaRepository로 전환 --- build.gradle | 2 +- .../JdbcReservationRepository.java | 238 ------------------ .../domain/reservation/Reservation.java | 43 +++- .../reservation/ReservationRepository.java | 26 +- .../reservation/ReservationService.java | 28 ++- .../JdbcReservationDateRepository.java | 83 ------ .../reservationdate/ReservationDate.java | 15 +- .../ReservationDateRepository.java | 13 +- .../ReservationDateService.java | 7 +- .../JdbcReservationTimeRepository.java | 83 ------ .../reservationtime/ReservationTime.java | 15 +- .../ReservationTimeRepository.java | 13 +- .../ReservationTimeService.java | 5 +- .../domain/theme/JdbcThemeRepository.java | 95 ------- .../java/roomescape/domain/theme/Theme.java | 19 +- .../domain/theme/ThemeRepository.java | 23 +- .../roomescape/domain/theme/ThemeService.java | 5 +- .../JdbcWaitingReservationRepository.java | 164 ------------ .../WaitingReservation.java | 44 +++- .../WaitingReservationRepository.java | 50 +++- .../WaitingReservationService.java | 17 +- .../dto/RankProjection.java | 6 + .../dto/WaitingReservationWithRank.java | 2 +- src/main/resources/application.yml | 7 + src/main/resources/schema.sql | 51 ---- .../domain/reservation/ReservationTest.java | 18 ++ .../ReservationUpdatingIntegrationTest.java | 19 -- .../JdbcWaitingReservationRepositoryTest.java | 174 ------------- .../WaitingReservationServiceTest.java | 10 +- 29 files changed, 251 insertions(+), 1024 deletions(-) delete mode 100644 src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java delete mode 100644 src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java delete mode 100644 src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java delete mode 100644 src/main/java/roomescape/domain/theme/JdbcThemeRepository.java delete mode 100644 src/main/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepository.java create mode 100644 src/main/java/roomescape/domain/waitingreservation/dto/RankProjection.java delete mode 100644 src/main/resources/schema.sql delete mode 100644 src/test/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepositoryTest.java diff --git a/build.gradle b/build.gradle index aeaee3cb94..8849aab41e 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ configurations { 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-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java deleted file mode 100644 index 79d5189e23..0000000000 --- a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java +++ /dev/null @@ -1,238 +0,0 @@ -package roomescape.domain.reservation; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationtime.ReservationTime; -import roomescape.domain.theme.Theme; - -@Repository -@RequiredArgsConstructor -public class JdbcReservationRepository implements ReservationRepository { - - private static final String INSERT_SQL = "insert into reservation(name, date_id, time_id, theme_id) values (?, ?, ?, ?)"; - private static final String FIND_ALL_SQL = - """ - select r.id, r.name, - rd.id as date_id, rd.play_day, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from reservation r - join reservation_date rd on r.date_id = rd.id - join reservation_time rt on r.time_id = rt.id - join theme th on r.theme_id = th.id - order by r.id - """; - private static final String COUNT_BY_TIME_ID_SQL = - """ - select count(*) - from reservation - where time_id = ? - """; - private static final String COUNT_BY_RESERVATION_DATE_ID_SQL = - """ - select count(*) - from reservation - where date_id = ? - """; - private static final String DELETE_BY_ID_SQL = "delete from reservation where id = ?"; - private static final String FIND_BY_THEME_AND_DATE_SQL = - """ - select time_id - from reservation - where theme_id = ? and date_id = ? - """; - - private static final String COUNT_BY_THEME_ID_SQL = - """ - select count(*) - from reservation - where theme_id = ? - """; - ; - - private static final String FIND_BY_NAME_SQL = - """ - select r.id, r.name, - rd.id as date_id, rd.play_day, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from reservation r - join reservation_date rd on r.date_id = rd.id - join reservation_time rt on r.time_id = rt.id - join theme th on r.theme_id = th.id - where r.name = ? - order by rd.play_day - """; - - private static final String FIND_BY_ID_SQL = - """ - select r.id, r.name, - rd.id as date_id, rd.play_day, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from reservation r - join reservation_date rd on r.date_id = rd.id - join reservation_time rt on r.time_id = rt.id - join theme th on r.theme_id = th.id - where r.id = ? - """; - - private static final String UPDATE_DATE_TIME_SQL = - """ - update reservation - set date_id = ?, time_id = ? - where id = ? - """; - - private static final String EXIST_BY_DATE_TIME_THEME_SQL = - """ - select exists( - select 1 - from reservation - where date_id = ? and time_id = ? and theme_id = ? - ); - """; - - private static final String EXIST_BY_NAME_DATE_TIME_THEME_SQL = - """ - select exists( - select 1 - from reservation - where name = ? and date_id = ? and time_id = ? and theme_id = ? - ); - """; - - private final JdbcTemplate jdbcTemplate; - - @Override - public Reservation save(Reservation reservation) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setString(1, reservation.getName()); - ps.setLong(2, reservation.getDate().getId()); - ps.setLong(3, reservation.getTime().getId()); - ps.setLong(4, reservation.getTheme().getId()); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return Reservation.of( - id, - reservation.getName(), - reservation.getDate(), - reservation.getTime(), - reservation.getTheme() - ); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, reservationRowMapper()); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - @Override - public int countByTimeId(Long timeId) { - Integer count = jdbcTemplate.queryForObject(COUNT_BY_TIME_ID_SQL, Integer.class, timeId); - if (count == null) { - return 0; - } - return count; - } - - @Override - public int countByReservationDateId(Long dateId) { - Integer count = jdbcTemplate.queryForObject(COUNT_BY_RESERVATION_DATE_ID_SQL, Integer.class, dateId); - if (count == null) { - return 0; - } - return count; - } - - @Override - public List findReservedTimes(Long themeId, Long dateId) { - return jdbcTemplate.query(FIND_BY_THEME_AND_DATE_SQL, reservationTimeIdRowMapper(), themeId, dateId); - } - - @Override - public int countByThemeId(Long themeId) { - Integer count = jdbcTemplate.queryForObject(COUNT_BY_THEME_ID_SQL, Integer.class, themeId); - if (count == null) { - return 0; - } - return count; - } - - @Override - public List findByName(String name) { - return jdbcTemplate.query(FIND_BY_NAME_SQL, reservationRowMapper(), name); - } - - @Override - public Optional findById(Long id) { - return jdbcTemplate.query(FIND_BY_ID_SQL, reservationRowMapper(), id) - .stream() - .findFirst(); - } - - @Override - public int updateReservation(Long id, Long dateId, Long timeId) { - return jdbcTemplate.update(UPDATE_DATE_TIME_SQL, dateId, timeId, id); - } - - @Override - public boolean existsByDateIdAndTimeIdAndThemeId(Long dateId, Long timeId, Long themeId) { - return jdbcTemplate.queryForObject(EXIST_BY_DATE_TIME_THEME_SQL, Boolean.class, dateId, timeId, themeId); - } - - @Override - public boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, Long dateId, Long timeId, Long themeId) { - return jdbcTemplate.queryForObject(EXIST_BY_NAME_DATE_TIME_THEME_SQL, Boolean.class, name, dateId, timeId, themeId); - } - - private RowMapper reservationRowMapper() { - return (rs, rowNum) -> Reservation.of( - rs.getLong("id"), - rs.getString("name"), - ReservationDate.of( - rs.getLong("date_id"), - LocalDate.parse(rs.getString("play_day"))), - ReservationTime.of( - rs.getLong("time_id"), - LocalTime.parse(rs.getString("start_at")) - ), - Theme.of( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_content"), - rs.getString("theme_url") - ) - ); - } - - private RowMapper reservationTimeIdRowMapper() { - return (rs, rowNum) -> rs.getLong("time_id"); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new IllegalStateException("생성 키를 조회할 수 없습니다."); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 64b434f2fc..f33d986412 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -1,5 +1,14 @@ package roomescape.domain.reservation; +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 jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.Getter; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; @@ -9,14 +18,33 @@ import roomescape.support.exception.RoomescapeException; import roomescape.support.exception.ThemeErrorCode; +@Entity +@Table(uniqueConstraints = { + @UniqueConstraint(columnNames = {"date_id", "time_id", "theme_id"}) +}) @Getter public class Reservation { - private final Long id; - private final String name; - private final ReservationDate date; - private final ReservationTime time; - private final Theme theme; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "date_id") + private ReservationDate date; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "time_id") + private ReservationTime time; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "theme_id") + private Theme theme; + + protected Reservation() { + + } private Reservation( Long id, @@ -67,6 +95,11 @@ public static Reservation of( ); } + public void update(ReservationDate date, ReservationTime time) { + this.date = date; + this.time = time; + } + private static void validate(String name, ReservationDate date, ReservationTime time, Theme theme) { if (name == null || name.isBlank()) { throw new RoomescapeException(ReservationErrorCode.INVALID_RESERVATION_NAME); diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index 9c9c462a51..c8fd1d39ff 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -1,30 +1,26 @@ package roomescape.domain.reservation; import java.util.List; -import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; -public interface ReservationRepository { +public interface ReservationRepository extends JpaRepository { - Reservation save(Reservation reservation); + Long countByTimeId(Long timeId); - List findAll(); - - int deleteById(Long id); - - int countByTimeId(Long timeId); - - int countByReservationDateId(Long dateId); + Long countByDateId(Long dateId); + @Query(""" + select r.time.id + from Reservation r + where r.theme.id = :themeId and r.date.id = :dateId + """) List findReservedTimes(Long themeId, Long dateId); - int countByThemeId(Long id); + Long countByThemeId(Long id); List findByName(String name); - Optional findById(Long id); - - int updateReservation(Long id, Long dateId, Long timeId); - boolean existsByDateIdAndTimeIdAndThemeId(Long dateId, Long timeId, Long themeId); boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, Long dateId, Long timeId, Long themeId); diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 6704d01079..d2c41c7103 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -46,6 +46,7 @@ public ReservationCreationResponse createReservation(ReservationCreationRequest return ReservationCreationResponse.from(savedReservation); } + @Transactional(readOnly = true) public List getAllReservations() { return reservationRepository.findAll() .stream() @@ -54,12 +55,10 @@ public List getAllReservations() { } public void deleteReservation(Long id) { - int deletedCount = reservationRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("이미 삭제된 예약 삭제 요청이 들어왔습니다. reservationId={}", id); - } + reservationRepository.deleteById(id); } + @Transactional(readOnly = true) public List getReservationsByName(String name) { return reservationRepository.findByName(name) .stream() @@ -73,6 +72,7 @@ public void cancelReservation(Long id) { validateModifiable(reservation.getDate(), reservation.getTime()); reservationRepository.deleteById(id); + reservationRepository.flush(); promoteWaitingReservation(reservation); } @@ -88,11 +88,12 @@ public ReservationResponse updateReservation(Long id, @Valid ReservationUpdateRe validateNotDuplicated(request.dateId(), request.timeId(), reservation.getTheme() .getId()); - int updatedCount = reservationRepository.updateReservation(id, request.dateId(), request.timeId()); - if (updatedCount == 0) { - log.warn(" 수정할 예약 건이 없습니다. reservationId={}", id); - } - promoteWaitingReservation(reservation); + ReservationDate originalDate = reservation.getDate(); + ReservationTime originalTime = reservation.getTime(); + reservation.update(newReservationDate, newReservationTime); + reservationRepository.flush(); + + promoteWaitingReservationBySlot(originalDate, originalTime, reservation.getTheme()); return ReservationResponse.from(findById(id)); } @@ -125,11 +126,12 @@ private void validateModifiable(ReservationDate reservationDate, ReservationTime } private void promoteWaitingReservation(Reservation reservation) { + promoteWaitingReservationBySlot(reservation.getDate(), reservation.getTime(), reservation.getTheme()); + } + + private void promoteWaitingReservationBySlot(ReservationDate date, ReservationTime time, Theme theme) { Optional waitingReservationOpt = waitingReservationRepository.findOldestBySlot( - reservation.getDate() - .getId(), reservation.getTime() - .getId(), reservation.getTheme() - .getId()); + date.getId(), time.getId(), theme.getId()); if (waitingReservationOpt.isPresent()) { WaitingReservation waitingReservation = waitingReservationOpt.get(); reservationRepository.save( diff --git a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java deleted file mode 100644 index a8a93f863c..0000000000 --- a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java +++ /dev/null @@ -1,83 +0,0 @@ -package roomescape.domain.reservationdate; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class JdbcReservationDateRepository implements ReservationDateRepository { - - private static final String INSERT_SQL = "insert into reservation_date(play_day) values (?)"; - private static final String FIND_BY_ID_SQL = "select id, play_day from reservation_date where id = ?"; - private static final String FIND_ALL_SQL = "select id, play_day from reservation_date order by id"; - private static final String DELETE_BY_ID_SQL = "delete from reservation_date where id = ?"; - private static final String EXISTS_BY_PLAY_DAY_SQL = - """ - select exists( - select 1 - from reservation_date - where play_day = ? - ) - """; - - private final JdbcTemplate jdbcTemplate; - - @Override - public Optional findById(Long id) { - List result = jdbcTemplate.query(FIND_BY_ID_SQL, reservationDateRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, reservationDateRowMapper()); - } - - @Override - public ReservationDate save(ReservationDate reservationDate) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setString(1, reservationDate.getPlayDay().toString()); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return ReservationDate.of( - id, - reservationDate.getPlayDay() - ); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - @Override - public boolean existsByPlayDay(LocalDate playDay) { - return jdbcTemplate.queryForObject(EXISTS_BY_PLAY_DAY_SQL, Boolean.class, playDay); - } - - private RowMapper reservationDateRowMapper() { - return (rs, rowNum) -> ReservationDate.of( - rs.getLong("id"), - LocalDate.parse(rs.getString("play_day")) - ); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new IllegalStateException("생성 키를 조회할 수 없습니다."); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDate.java b/src/main/java/roomescape/domain/reservationdate/ReservationDate.java index 16d4781f27..3c07d70660 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDate.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDate.java @@ -1,5 +1,9 @@ package roomescape.domain.reservationdate; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import java.time.LocalDate; import java.time.LocalDateTime; import lombok.Getter; @@ -7,11 +11,18 @@ import roomescape.support.exception.ReservationDateErrorCode; import roomescape.support.exception.RoomescapeException; +@Entity @Getter public class ReservationDate { - private final Long id; - private final LocalDate playDay; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private LocalDate playDay; + + protected ReservationDate() { + + } private ReservationDate(Long id, LocalDate playDay) { validate(playDay); diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java index 7b92ee282e..03a843d344 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java @@ -1,18 +1,9 @@ package roomescape.domain.reservationdate; import java.time.LocalDate; -import java.util.List; -import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; -public interface ReservationDateRepository { - - Optional findById(Long id); - - List findAll(); - - ReservationDate save(ReservationDate reservationDate); - - int deleteById(Long id); +public interface ReservationDateRepository extends JpaRepository { boolean existsByPlayDay(LocalDate playDay); } diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java index 0c8af34b05..f9ab21112b 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java @@ -36,13 +36,10 @@ public ReservationDateCreationResponse createReservationDate(ReservationDateCrea } public void deleteReservationDate(Long id) { - if (reservationRepository.countByReservationDateId(id) > 0) { + if (reservationRepository.countByDateId(id) > 0) { throw new RoomescapeException(ReservationDateErrorCode.RESERVATION_DATE_IN_USE); } - int deletedCount = reservationDateRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("이미 삭제된 날짜의 삭제 요청이 들어왔습니다. dateId={}", id); - } + reservationDateRepository.deleteById(id); } public List getAllAvailableReservationDate() { diff --git a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java deleted file mode 100644 index 84845cbe37..0000000000 --- a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java +++ /dev/null @@ -1,83 +0,0 @@ -package roomescape.domain.reservationtime; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.time.LocalTime; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class JdbcReservationTimeRepository implements ReservationTimeRepository { - - private static final String INSERT_SQL = "insert into reservation_time(start_at) values (?)"; - private static final String FIND_ALL_SQL = "select id, start_at from reservation_time order by id"; - private static final String FIND_BY_ID_SQL = "select id, start_at from reservation_time where id = ?"; - private static final String DELETE_BY_ID_SQL = "delete from reservation_time where id = ?"; - private static final String EXISTS_BY_START_AT_SQL = - """ - select exists( - select 1 - from reservation_time - where start_at = ? - ) - """; - - private final JdbcTemplate jdbcTemplate; - - @Override - public ReservationTime save(ReservationTime reservationTime) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setString(1, reservationTime.getFormattedStartAt()); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return ReservationTime.of( - id, - reservationTime.getStartAt() - ); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, reservationTimeRowMapper()); - } - - @Override - public Optional findById(Long id) { - List result = jdbcTemplate.query(FIND_BY_ID_SQL, reservationTimeRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - @Override - public boolean existsByStartAt(LocalTime startAt) { - return jdbcTemplate.queryForObject(EXISTS_BY_START_AT_SQL, Boolean.class, startAt); - } - - private RowMapper reservationTimeRowMapper() { - return (rs, rowNum) -> ReservationTime.of( - rs.getLong("id"), - LocalTime.parse(rs.getString("start_at")) - ); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new IllegalStateException("생성 키를 조회할 수 없습니다."); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java index 6e2158e69f..2c4299ea0d 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java @@ -1,18 +1,29 @@ package roomescape.domain.reservationtime; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import lombok.Getter; import roomescape.support.exception.ReservationTimeErrorCode; import roomescape.support.exception.RoomescapeException; +@Entity @Getter public class ReservationTime { private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); - private final Long id; - private final LocalTime startAt; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private LocalTime startAt; + + protected ReservationTime() { + + } private ReservationTime(Long id, LocalTime startAt) { validate(startAt); diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java index 93af6a08d9..d21f7803ee 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java @@ -1,18 +1,9 @@ package roomescape.domain.reservationtime; import java.time.LocalTime; -import java.util.List; -import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; -public interface ReservationTimeRepository { - - ReservationTime save(ReservationTime reservationTime); - - List findAll(); - - Optional findById(Long id); - - int deleteById(Long id); +public interface ReservationTimeRepository extends JpaRepository { boolean existsByStartAt(LocalTime startAt); } diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java index ad1cc80e49..85df8169b7 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java @@ -44,10 +44,7 @@ public void deleteReservationTime(Long id) { if (reservationRepository.countByTimeId(id) > 0) { throw new RoomescapeException(ReservationTimeErrorCode.RESERVATION_TIME_IN_USE); } - int deletedCount = reservationTimeRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("이미 삭제된 예약 시간 삭제 요청이 들어왔습니다. timeId={}", id); - } + reservationTimeRepository.deleteById(id); } public List getReservationTimeAvailability(Long themeId, Long dateId) { diff --git a/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java b/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java deleted file mode 100644 index 304bb3b2aa..0000000000 --- a/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java +++ /dev/null @@ -1,95 +0,0 @@ -package roomescape.domain.theme; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.support.exception.RoomescapeErrorCode; -import roomescape.support.exception.RoomescapeException; - -@Repository -@RequiredArgsConstructor -public class JdbcThemeRepository implements ThemeRepository { - - private static final String FIND_ALL_SQL = "select id, name, content, url from theme order by id"; - private static final String FIND_BY_ID_SQL = "select id, name, content, url from theme where id = ?"; - private static final String INSERT_SQL = "insert into theme(name, content, url) values (?, ?, ?)"; - private static final String DELETE_BY_ID_SQL = "delete from theme where id = ?"; - private static final String FIND_POPULAR_THEMES_SQL = - """ - select th.id, th.name, th.content, th.url - from theme th - join reservation r on th.id = r.theme_id - join reservation_date rd on r.date_id = rd.id - where rd.play_day between ? and ? - group by th.id - order by count(r.id) desc, th.id asc - limit ? - """; - - private final JdbcTemplate jdbcTemplate; - - @Override - public Optional findById(Long id) { - List result = jdbcTemplate.query(FIND_BY_ID_SQL, themeRowMapper(), id); - return result.stream().findFirst(); - } - - @Override - public List findAll() { - return jdbcTemplate.query(FIND_ALL_SQL, themeRowMapper()); - } - - @Override - public Theme save(Theme theme) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setString(1, theme.getName()); - ps.setString(2, theme.getContent()); - ps.setString(3, theme.getUrl()); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return Theme.of( - id, - theme.getName(), - theme.getContent(), - theme.getUrl() - ); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - @Override - public List findPopularThemes(int rankLimit, LocalDate startDay, LocalDate endDay) { - return jdbcTemplate.query(FIND_POPULAR_THEMES_SQL, themeRowMapper(), startDay, endDay, rankLimit); - } - - private RowMapper themeRowMapper() { - return ((rs, rowNum) -> Theme.of( - rs.getLong("id"), - rs.getString("name"), - rs.getString("content"), - rs.getString("url") - )); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new RoomescapeException(RoomescapeErrorCode.INVALID_GENERATED_KEY); - } - return keyHolder.getKey().longValue(); - } - -} diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index 438fd8316d..597e5c5882 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -1,16 +1,27 @@ package roomescape.domain.theme; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import lombok.Getter; import roomescape.support.exception.RoomescapeException; import roomescape.support.exception.ThemeErrorCode; +@Entity @Getter public class Theme { - private final Long id; - private final String name; - private final String content; - private final String url; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String content; + private String url; + + protected Theme() { + + } private Theme(Long id, String name, String content, String url) { validate(name, content, url); diff --git a/src/main/java/roomescape/domain/theme/ThemeRepository.java b/src/main/java/roomescape/domain/theme/ThemeRepository.java index 0e6a9e6a34..f3841a5f16 100644 --- a/src/main/java/roomescape/domain/theme/ThemeRepository.java +++ b/src/main/java/roomescape/domain/theme/ThemeRepository.java @@ -1,18 +1,19 @@ package roomescape.domain.theme; import java.time.LocalDate; -import java.util.Optional; import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; -public interface ThemeRepository { +public interface ThemeRepository extends JpaRepository { - Optional findById(Long id); - - List findAll(); - - Theme save(Theme theme); - - int deleteById(Long id); - - List findPopularThemes(int rankLimit, LocalDate startDay, LocalDate endDay); + @Query(""" + select t from Theme t + join Reservation r on t = r.theme + where r.date.playDay between :startDate and :endDate + group by t + order by count(r) desc, t.id asc + limit :rankLimit + """) + List findPopularThemes(int rankLimit, LocalDate startDate, LocalDate endDate); } diff --git a/src/main/java/roomescape/domain/theme/ThemeService.java b/src/main/java/roomescape/domain/theme/ThemeService.java index 256c12821c..54bc1bfa8c 100644 --- a/src/main/java/roomescape/domain/theme/ThemeService.java +++ b/src/main/java/roomescape/domain/theme/ThemeService.java @@ -41,10 +41,7 @@ public void deleteTheme(Long id) { if (reservationRepository.countByThemeId(id) > 0) { throw new RoomescapeException(ThemeErrorCode.THEME_IN_USE); } - int deletedCount = themeRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("삭제할 테마가 존재하지 않습니다. themeId = {}", id); - } + themeRepository.deleteById(id); } public List getAllTheme() { diff --git a/src/main/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepository.java b/src/main/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepository.java deleted file mode 100644 index 132d734bb2..0000000000 --- a/src/main/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepository.java +++ /dev/null @@ -1,164 +0,0 @@ -package roomescape.domain.waitingreservation; - -import java.sql.PreparedStatement; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationtime.ReservationTime; -import roomescape.domain.theme.Theme; -import roomescape.domain.waitingreservation.dto.WaitingReservationWithRank; - -@Repository -@RequiredArgsConstructor -public class JdbcWaitingReservationRepository implements WaitingReservationRepository { - - private static final String INSERT_SQL = "insert into waiting_reservation(name, date_id, time_id, theme_id, created_at) values (?, ?, ?, ?, ?)"; - - private static final String EXIST_BY_NAME_DATE_TIME_THEME_SQL = """ - select exists( - select 1 - from waiting_reservation - where name = ? and date_id = ? and time_id = ? and theme_id = ? - ); - """; - - private static final String FIND_OLDEST_BY_SLOT_SQL = """ - select wr.id, wr.name, wr.created_at, - rd.id as date_id, rd.play_day, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from waiting_reservation wr - join reservation_date rd on wr.date_id = rd.id - join reservation_time rt on wr.time_id = rt.id - join theme th on wr.theme_id = th.id - where wr.date_id = ? and wr.time_id = ? and wr.theme_id = ? - order by wr.created_at asc, wr.id asc - limit 1 - for update - """; - - private static final String FIND_BY_NAME_SQL = """ - select wr.id, wr.name, wr.created_at, - rd.id as date_id, rd.play_day, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url - from waiting_reservation wr - join reservation_date rd on wr.date_id = rd.id - join reservation_time rt on wr.time_id = rt.id - join theme th on wr.theme_id = th.id - where wr.name = ? - order by rd.play_day asc, rt.start_at asc, wr.created_at asc - """; - - private static final String FIND_RANK_BY_SLOT_SQL = """ - select wr.id, - row_number() over ( - partition by wr.date_id, wr.time_id, wr.theme_id - order by wr.created_at asc, wr.id asc - ) as rank - from waiting_reservation wr - inner join ( - select date_id, time_id, theme_id - from waiting_reservation - where name = ? - ) slot - on wr.date_id = slot.date_id and wr.time_id = slot.time_id and wr.theme_id = slot.theme_id - """; - - private static final String DELETE_BY_ID_SQL = "delete from waiting_reservation where id = ?"; - - private static final String FIND_BY_ID_SQL = """ - select wr.id, wr.name, - rd.id as date_id, rd.play_day, - rt.id as time_id, rt.start_at, - th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url, - wr.created_at - from waiting_reservation wr - join reservation_date rd on wr.date_id = rd.id - join reservation_time rt on wr.time_id = rt.id - join theme th on wr.theme_id = th.id - where wr.id = ? - """; - private final JdbcTemplate jdbcTemplate; - - @Override - public WaitingReservation save(WaitingReservation waitingReservation) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS); - ps.setString(1, waitingReservation.getName()); - ps.setLong(2, waitingReservation.getDate().getId()); - ps.setLong(3, waitingReservation.getTime().getId()); - ps.setLong(4, waitingReservation.getTheme().getId()); - ps.setTimestamp(5, Timestamp.valueOf(waitingReservation.getCreatedAt())); - return ps; - }, keyHolder); - long id = extractId(keyHolder); - return WaitingReservation.of(id, waitingReservation.getName(), waitingReservation.getDate(), - waitingReservation.getTime(), waitingReservation.getTheme(), waitingReservation.getCreatedAt()); - } - - @Override - public boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, long dateId, long timeId, long themeId) { - return jdbcTemplate.queryForObject(EXIST_BY_NAME_DATE_TIME_THEME_SQL, Boolean.class, name, dateId, timeId, - themeId); - } - - @Override - public Optional findOldestBySlot(long dateId, long timeId, long themeId) { - return jdbcTemplate.query(FIND_OLDEST_BY_SLOT_SQL, waitingReservationRowMapper(), dateId, timeId, themeId) - .stream().findFirst(); - } - - @Override - public List findAllByNameWithRank(String name) { - List waitingReservations = jdbcTemplate.query(FIND_BY_NAME_SQL, waitingReservationRowMapper(), name); - Map rankMap = jdbcTemplate.query(FIND_RANK_BY_SLOT_SQL, - (rs, rowNum) -> Map.entry( - rs.getLong("id"), - rs.getLong("rank")), - name) - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return waitingReservations.stream() - .map(wr -> new WaitingReservationWithRank(wr, rankMap.get(wr.getId()))) - .toList(); - } - - @Override - public int deleteById(Long id) { - return jdbcTemplate.update(DELETE_BY_ID_SQL, id); - } - - @Override - public Optional findById(Long id) { - return jdbcTemplate.query(FIND_BY_ID_SQL, waitingReservationRowMapper(), id).stream().findFirst(); - } - - private RowMapper waitingReservationRowMapper() { - return (rs, rowNum) -> WaitingReservation.of(rs.getLong("id"), rs.getString("name"), - ReservationDate.of(rs.getLong("date_id"), LocalDate.parse(rs.getString("play_day"))), - ReservationTime.of(rs.getLong("time_id"), LocalTime.parse(rs.getString("start_at"))), - Theme.of(rs.getLong("theme_id"), rs.getString("theme_name"), rs.getString("theme_content"), - rs.getString("theme_url")), rs.getTimestamp("created_at").toLocalDateTime()); - } - - private long extractId(KeyHolder keyHolder) { - if (keyHolder.getKey() == null) { - throw new IllegalStateException("생성 키를 조회할 수 없습니다."); - } - return keyHolder.getKey().longValue(); - } -} diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservation.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservation.java index eece7e79f8..02d631c84b 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservation.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservation.java @@ -1,5 +1,14 @@ package roomescape.domain.waitingreservation; +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 jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import java.time.LocalDateTime; import lombok.Getter; import roomescape.domain.reservationdate.ReservationDate; @@ -8,15 +17,35 @@ import roomescape.support.exception.RoomescapeException; import roomescape.support.exception.WaitingReservationErrorCode; +@Entity +@Table(uniqueConstraints = { + @UniqueConstraint(columnNames = {"name", "date_id", "time_id", "theme_id"}) +}) @Getter public class WaitingReservation { - private final Long id; - private final String name; - private final ReservationDate date; - private final ReservationTime time; - private final Theme theme; - private final LocalDateTime createdAt; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "date_id") + private ReservationDate date; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "time_id") + private ReservationTime time; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "theme_id") + private Theme theme; + + private LocalDateTime createdAt; + + protected WaitingReservation() { + + } private WaitingReservation(Long id, String name, ReservationDate date, ReservationTime time, Theme theme, LocalDateTime createdAt) { @@ -29,7 +58,8 @@ private WaitingReservation(Long id, String name, ReservationDate date, Reservati this.createdAt = createdAt; } - public static WaitingReservation createWithoutId(String name, ReservationDate date, ReservationTime time, Theme theme, LocalDateTime createdAt) { + public static WaitingReservation createWithoutId(String name, ReservationDate date, ReservationTime time, + Theme theme, LocalDateTime createdAt) { return new WaitingReservation(null, name, date, time, theme, createdAt); } diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationRepository.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationRepository.java index 58f9915fb3..10826dad47 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationRepository.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationRepository.java @@ -1,20 +1,44 @@ package roomescape.domain.waitingreservation; +import jakarta.persistence.LockModeType; import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import roomescape.domain.waitingreservation.dto.RankProjection; import roomescape.domain.waitingreservation.dto.WaitingReservationWithRank; -public interface WaitingReservationRepository { - - WaitingReservation save(WaitingReservation waitingReservation); - - boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, long dateId, long timeId, long themeId); - - Optional findOldestBySlot(long dateId, long timeId, long themeId); - - List findAllByNameWithRank(String name); - - int deleteById(Long id); - - Optional findById(Long id); +public interface WaitingReservationRepository extends JpaRepository { + + boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, Long dateId, Long timeId, Long themeId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query(""" + select wr + from WaitingReservation wr + join fetch wr.date + join fetch wr.time + join fetch wr.theme + where wr.date.id = :dateId and wr.time.id = :timeId and wr.theme.id = :themeId + order by wr.createdAt asc, wr.id asc + limit 1 + """) + Optional findOldestBySlot(Long dateId, Long timeId, Long themeId); + + List findAllByName(String name); + + @Query(value = """ + select wr.id, row_number() over ( + partition by wr.date_id, wr.time_id, wr.theme_id + order by wr.created_at asc, wr.id asc + ) as rank + from waiting_reservation wr + join ( + select date_id, time_id, theme_id + from waiting_reservation + where name = :name + ) slot on wr.date_id = slot.date_id and wr.time_id = slot.time_id and wr.theme_id = slot.theme_id + """, nativeQuery = true) + List findRankByName(String name); } diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java index 6d280c46e5..c1a69d5358 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java @@ -2,6 +2,8 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -12,8 +14,10 @@ import roomescape.domain.reservationtime.ReservationTimeService; import roomescape.domain.theme.Theme; import roomescape.domain.theme.ThemeService; +import roomescape.domain.waitingreservation.dto.RankProjection; import roomescape.domain.waitingreservation.dto.WaitingReservationCreationRequest; import roomescape.domain.waitingreservation.dto.WaitingReservationCreationResponse; +import roomescape.domain.waitingreservation.dto.WaitingReservationWithRank; import roomescape.domain.waitingreservation.dto.WaitingReservationWithRankResponse; import roomescape.support.exception.ReservationDateErrorCode; import roomescape.support.exception.RoomescapeException; @@ -58,15 +62,18 @@ public void cancelWaitingReservation(Long id) { throw new RoomescapeException(WaitingReservationErrorCode.ALREADY_PROMOTED_TO_RESERVATION); } - int deletedCount = waitingReservationRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("이미 삭제된 예약 대기 삭제 요청이 들어왔습니다. reservationId={}", id); - } + waitingReservationRepository.deleteById(id); } public List getWaitingReservationsWithRankByName(String name) { - return waitingReservationRepository.findAllByNameWithRank(name) + List waitingReservations = waitingReservationRepository.findAllByName(name); + + Map rankMap = waitingReservationRepository.findRankByName(name) .stream() + .collect(Collectors.toMap(RankProjection::getId, RankProjection::getRank)); + + return waitingReservations.stream() + .map(wr -> new WaitingReservationWithRank(wr, rankMap.get(wr.getId()))) .map(WaitingReservationWithRankResponse::from) .toList(); } diff --git a/src/main/java/roomescape/domain/waitingreservation/dto/RankProjection.java b/src/main/java/roomescape/domain/waitingreservation/dto/RankProjection.java new file mode 100644 index 0000000000..cb1a23e194 --- /dev/null +++ b/src/main/java/roomescape/domain/waitingreservation/dto/RankProjection.java @@ -0,0 +1,6 @@ +package roomescape.domain.waitingreservation.dto; + +public interface RankProjection { + Long getId(); + Long getRank(); +} diff --git a/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationWithRank.java b/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationWithRank.java index 921e0e7d5b..c736cd7374 100644 --- a/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationWithRank.java +++ b/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationWithRank.java @@ -4,6 +4,6 @@ public record WaitingReservationWithRank( WaitingReservation waitingReservation, - long rank + Long rank ) { } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ebd907f5af..5f16c343b5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,5 +5,12 @@ spring: path: /h2-console datasource: url: jdbc:h2:mem:database + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + ddl-auto: create-drop + defer-datasource-initialization: true token: ${ADMIN_TOKEN:sanwhale0192} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index 63dfc0b15e..0000000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,51 +0,0 @@ -CREATE TABLE IF NOT EXISTS reservation_time -( - id BIGINT NOT NULL AUTO_INCREMENT, - start_at VARCHAR(255) NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS reservation_date -( - id BIGINT NOT NULL AUTO_INCREMENT, - play_day VARCHAR(255) NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS theme -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - content VARCHAR(255) NOT NULL, - url VARCHAR(255) NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS reservation -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - date_id BIGINT NOT NULL, - time_id BIGINT NOT NULL, - theme_id BIGINT NOT NULL, - PRIMARY KEY (id), - UNIQUE(date_id, time_id, theme_id), - FOREIGN KEY (time_id) REFERENCES reservation_time (id), - FOREIGN KEY (date_id) REFERENCES reservation_date (id), - FOREIGN KEY (theme_id) REFERENCES theme (id) -); - -CREATE TABLE IF NOT EXISTS waiting_reservation -( - id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - date_id BIGINT NOT NULL, - time_id BIGINT NOT NULL, - theme_id BIGINT NOT NULL, - created_at TIMESTAMP NOT NULL, - PRIMARY KEY (id), - UNIQUE (name, date_id, time_id, theme_id), - FOREIGN KEY (time_id) REFERENCES reservation_time (id), - FOREIGN KEY (date_id) REFERENCES reservation_date (id), - FOREIGN KEY (theme_id) REFERENCES theme (id) -); diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 413559351e..30ca648d9c 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -161,4 +161,22 @@ class ReservationTest { .isInstanceOf(RoomescapeException.class) .hasMessage("테마 엔티티 식별자 정보가 누락되었습니다."); } + + @Test + void 날짜_시간을_업데이트_할_수_있다() { + // given + ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); + ReservationTime updateTime = ReservationTime.createWithoutId(LocalTime.of(10, 00)); + ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2026, 8, 5)); + ReservationDate updateDate = ReservationDate.createWithoutId(LocalDate.of(2026, 8, 15)); + Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); + Reservation reservation = Reservation.createWithoutId("이산", date, time, theme); + + // when + reservation.update(updateDate, updateTime); + + // then + assertThat(reservation.getDate()).isEqualTo(updateDate); + assertThat(reservation.getTime()).isEqualTo(updateTime); + } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java index 9039fc7f11..3814f472e1 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java @@ -14,7 +14,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.jdbc.Sql; -import roomescape.domain.reservation.dto.ReservationResponse; import roomescape.domain.reservation.dto.ReservationUpdateRequest; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; @@ -88,24 +87,6 @@ void setUp() { assertThat(waitingReservationRepository.findById(secondWaiting.getId())).isPresent(); } - @Test - void 예약_수정_중_예약_수정이_실패하면_전체가_롤백된다() { - ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); - - WaitingReservation firstWaiting = waitingReservationRepository.save( - waiting("이산", originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) - ); - - doThrow(new RuntimeException()).when(reservationRepository).updateReservation(originReservation.getId(), updateRequest.dateId(), updateRequest.timeId()); - - assertThatThrownBy(() -> reservationService.updateReservation(originReservation.getId(), updateRequest)) - .isInstanceOf(RuntimeException.class); - - assertThat(reservationRepository.findById(originReservation.getId())).isPresent(); - assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); - assertThat(reservationRepository.findByName("이산")).isEmpty(); - } - @Test void 예약_수정_중_1순위_예약_대기_추가가_실패하면_전체가_롤백된다() { ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); diff --git a/src/test/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepositoryTest.java b/src/test/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepositoryTest.java deleted file mode 100644 index 2d0af7105f..0000000000 --- a/src/test/java/roomescape/domain/waitingreservation/JdbcWaitingReservationRepositoryTest.java +++ /dev/null @@ -1,174 +0,0 @@ -package roomescape.domain.waitingreservation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; -import org.springframework.context.annotation.Import; -import org.springframework.dao.DuplicateKeyException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.jdbc.Sql; -import roomescape.domain.reservationdate.JdbcReservationDateRepository; -import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationdate.ReservationDateRepository; -import roomescape.domain.reservationtime.JdbcReservationTimeRepository; -import roomescape.domain.reservationtime.ReservationTime; -import roomescape.domain.reservationtime.ReservationTimeRepository; -import roomescape.domain.theme.JdbcThemeRepository; -import roomescape.domain.theme.Theme; -import roomescape.domain.theme.ThemeRepository; -import roomescape.domain.waitingreservation.dto.WaitingReservationWithRank; - -@JdbcTest -@Sql("/truncate.sql") -@Import({JdbcReservationDateRepository.class, JdbcReservationTimeRepository.class, JdbcThemeRepository.class}) -class JdbcWaitingReservationRepositoryTest { - - private static final LocalDate PLAY_DAY = LocalDate.of(2026, 5, 10); - private static final LocalTime START_AT = LocalTime.of(10, 0); - - @Autowired - private JdbcTemplate jdbcTemplate; - - @Autowired - private ReservationDateRepository reservationDateRepository; - - @Autowired - private ReservationTimeRepository reservationTimeRepository; - - @Autowired - private ThemeRepository themeRepository; - - private WaitingReservationRepository waitingReservationRepository; - private ReservationDate date; - private ReservationTime time; - private Theme theme; - - @BeforeEach - void setUp() { - waitingReservationRepository = new JdbcWaitingReservationRepository(jdbcTemplate); - - date = reservationDateRepository.save(ReservationDate.createWithoutId(PLAY_DAY)); - time = reservationTimeRepository.save(ReservationTime.createWithoutId(START_AT)); - theme = themeRepository.save(Theme.createWithoutId("공포", "테마 내용", "/themes/scary")); - } - - @Test - void 같은_이름_날짜_테마_시간으로_예약_대기를_생성할_수_없다() { - WaitingReservation waitingReservation = waiting("이산", LocalDateTime.of(2026, 5, 9, 10, 0)); - waitingReservationRepository.save(waitingReservation); - - assertThatThrownBy(() -> waitingReservationRepository.save(waitingReservation)) - .isInstanceOf(DuplicateKeyException.class); - } - - @Test - void 가장_먼저_신청한_예약_대기를_가져온다() { - waitingReservationRepository.save(waiting("이산", LocalDateTime.of(2026, 5, 7, 10, 0))); - waitingReservationRepository.save(waiting("고래", LocalDateTime.of(2026, 5, 8, 10, 0))); - waitingReservationRepository.save(waiting("보예", LocalDateTime.of(2026, 5, 9, 10, 0))); - - WaitingReservation oldest = waitingReservationRepository.findOldestBySlot( - date.getId(), - time.getId(), - theme.getId() - ).orElseThrow(); - - assertThat(oldest.getName()).isEqualTo("이산"); - assertThat(oldest.getDate().getId()).isEqualTo(date.getId()); - assertThat(oldest.getDate().getPlayDay()).isEqualTo(PLAY_DAY); - assertThat(oldest.getTime().getId()).isEqualTo(time.getId()); - assertThat(oldest.getTime().getStartAt()).isEqualTo(START_AT); - assertThat(oldest.getTheme().getId()).isEqualTo(theme.getId()); - assertThat(oldest.getTheme().getName()).isEqualTo("공포"); - assertThat(oldest.getCreatedAt()).isEqualTo(LocalDateTime.of(2026, 5, 7, 10, 0)); - } - - @Test - void 예약_대기가_없으면_가장_먼저_신청한_예약_대기를_조회할_수_없다() { - assertThat(waitingReservationRepository.findOldestBySlot( - date.getId(), - time.getId(), - theme.getId() - ) - ).isEmpty(); - } - - @Test - void 특정_슬롯에서_가장_먼저_신청한_예약_대기를_가져온다() { - Slot otherSlot = insertSlot(LocalDate.of(2026, 5, 11), LocalTime.of(11, 0), "스릴러"); - waitingReservationRepository.save(waiting("다른슬롯", otherSlot, LocalDateTime.of(2026, 5, 6, 10, 0))); - waitingReservationRepository.save(waiting("이산", LocalDateTime.of(2026, 5, 7, 10, 0))); - waitingReservationRepository.save(waiting("고래", LocalDateTime.of(2026, 5, 8, 10, 0))); - - WaitingReservation oldest = waitingReservationRepository - .findOldestBySlot(date.getId(), time.getId(), theme.getId()) - .orElseThrow(); - - assertThat(oldest.getName()).isEqualTo("이산"); - } - - @Test - void 사용자_이름으로_예약_대기_목록을_조회하면_각_슬롯의_순번을_반환한다() { - waitingReservationRepository.save(waiting("고래", LocalDateTime.of(2026, 5, 7, 10, 0))); - waitingReservationRepository.save(waiting("이산", LocalDateTime.of(2026, 5, 8, 10, 0))); - - Slot secondSlot = insertSlot(LocalDate.of(2026, 5, 11), LocalTime.of(11, 0), "스릴러"); - waitingReservationRepository.save(waiting("이산", secondSlot, LocalDateTime.of(2026, 5, 7, 11, 0))); - waitingReservationRepository.save(waiting("브리", secondSlot, LocalDateTime.of(2026, 5, 8, 11, 0))); - - Slot thirdSlot = insertSlot(LocalDate.of(2026, 5, 12), LocalTime.of(13, 0), "미스터리"); - waitingReservationRepository.save(waiting("나무", thirdSlot, LocalDateTime.of(2026, 5, 7, 12, 0))); - waitingReservationRepository.save(waiting("고래", thirdSlot, LocalDateTime.of(2026, 5, 8, 12, 0))); - waitingReservationRepository.save(waiting("이산", thirdSlot, LocalDateTime.of(2026, 5, 9, 12, 0))); - - List waitings = waitingReservationRepository.findAllByNameWithRank("이산"); - - assertThat(waitings).hasSize(3); - assertThat(waitings).extracting(result -> result.waitingReservation().getName()) - .containsOnly("이산"); - assertThat(waitings).extracting(result -> result.waitingReservation().getDate().getId()) - .containsExactly(date.getId(), secondSlot.date().getId(), thirdSlot.date().getId()); - assertThat(waitings).extracting(WaitingReservationWithRank::rank) - .containsExactly(2L, 1L, 3L); - } - - @Test - void 대기_취소를_하면_정상_삭제한다() { - WaitingReservation actual = waitingReservationRepository.save( - waiting("고래", LocalDateTime.of(2026, 5, 7, 10, 0))); - - waitingReservationRepository.deleteById(actual.getId()); - - assertThat(waitingReservationRepository.findById(actual.getId())).isEmpty(); - } - - private WaitingReservation waiting(String name, LocalDateTime createdAt) { - return WaitingReservation.createWithoutId(name, date, time, theme, createdAt); - } - - private WaitingReservation waiting(String name, Slot slot, LocalDateTime createdAt) { - return WaitingReservation.createWithoutId(name, slot.date(), slot.time(), slot.theme(), createdAt); - } - - private Slot insertSlot(LocalDate playDay, LocalTime startAt, String themeName) { - ReservationDate savedDate = reservationDateRepository.save(ReservationDate.createWithoutId(playDay)); - ReservationTime savedTime = reservationTimeRepository.save(ReservationTime.createWithoutId(startAt)); - Theme savedTheme = themeRepository.save(Theme.createWithoutId(themeName, "테마 내용", "/themes/" + themeName)); - return new Slot(savedDate, savedTime, savedTheme); - } - - private record Slot( - ReservationDate date, - ReservationTime time, - Theme theme - ) { - } -} diff --git a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java index 40af34a469..319d0deb39 100644 --- a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java +++ b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java @@ -21,9 +21,9 @@ import roomescape.domain.reservationtime.ReservationTimeService; import roomescape.domain.theme.Theme; import roomescape.domain.theme.ThemeService; +import roomescape.domain.waitingreservation.dto.RankProjection; import roomescape.domain.waitingreservation.dto.WaitingReservationCreationRequest; import roomescape.domain.waitingreservation.dto.WaitingReservationCreationResponse; -import roomescape.domain.waitingreservation.dto.WaitingReservationWithRank; import roomescape.domain.waitingreservation.dto.WaitingReservationWithRankResponse; import roomescape.support.exception.RoomescapeException; @@ -141,9 +141,13 @@ void setUp() { WaitingReservation waiting = WaitingReservation.of( 10L, "이산", date, time, theme, LocalDateTime.of(2026, 5, 5, 14, 0) ); - WaitingReservationWithRank withRank = new WaitingReservationWithRank(waiting, 5L); + RankProjection rankProjection = new RankProjection() { + public Long getId() { return 10L; } + public Long getRank() { return 5L; } + }; - when(waitingReservationRepository.findAllByNameWithRank("이산")).thenReturn(List.of(withRank)); + when(waitingReservationRepository.findAllByName("이산")).thenReturn(List.of(waiting)); + when(waitingReservationRepository.findRankByName("이산")).thenReturn(List.of(rankProjection)); List result = waitingReservationService .getWaitingReservationsWithRankByName("이산"); From 7429f541f71c7a379587d00d1b9ead52e5ea7e6f Mon Sep 17 00:00:00 2001 From: rin Date: Thu, 18 Jun 2026 16:38:30 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[2=EB=8B=A8=EA=B3=84]=20:=20=EB=82=B4=20?= =?UTF-8?q?=EC=98=88=EC=95=BD=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Member 도메인 추가 - Reservation, WaitingReservation의 name 필드를 Member 연관관계로 교체 - GET /reservations-mine 엔드포인트 추가 --- .../java/roomescape/domain/member/Member.java | 33 +++++ .../domain/member/MemberRepository.java | 6 + .../domain/member/MemberService.java | 18 +++ .../domain/reservation/Reservation.java | 60 +++----- .../reservation/ReservationController.java | 6 +- .../reservation/ReservationRepository.java | 8 +- .../reservation/ReservationService.java | 15 +- .../dto/ReservationCreationRequest.java | 16 +-- .../dto/ReservationCreationResponse.java | 2 +- .../reservation/dto/ReservationResponse.java | 2 +- .../WaitingReservation.java | 33 +++-- .../WaitingReservationController.java | 9 +- .../WaitingReservationRepository.java | 9 +- .../WaitingReservationService.java | 25 ++-- .../WaitingReservationCreationRequest.java | 9 +- .../WaitingReservationCreationResponse.java | 2 +- .../WaitingReservationWithRankResponse.java | 2 +- .../support/exception/MemberErrorCode.java | 27 ++++ src/main/resources/data.sql | 108 +++++++++----- .../AdminReservationControllerTest.java | 11 +- ...eservationCancellationIntegrationTest.java | 97 ++++++------- .../ReservationControllerTest.java | 135 ++++++++++-------- .../reservation/ReservationServiceTest.java | 92 +++++++----- .../domain/reservation/ReservationTest.java | 91 ++++-------- .../ReservationUpdatingIntegrationTest.java | 65 +++++---- .../dto/ReservationCreationRequestTest.java | 70 ++------- .../ReservationDateServiceTest.java | 8 +- .../ReservationTimeServiceTest.java | 11 +- .../domain/theme/ThemeServiceTest.java | 8 +- .../WaitingReservationControllerTest.java | 121 +++++++++------- ...ngReservationControllerValidationTest.java | 27 +--- .../WaitingReservationServiceTest.java | 41 +++--- .../WaitingReservationTest.java | 31 ++-- src/test/resources/truncate.sql | 2 + 34 files changed, 637 insertions(+), 563 deletions(-) create mode 100644 src/main/java/roomescape/domain/member/Member.java create mode 100644 src/main/java/roomescape/domain/member/MemberRepository.java create mode 100644 src/main/java/roomescape/domain/member/MemberService.java create mode 100644 src/main/java/roomescape/support/exception/MemberErrorCode.java diff --git a/src/main/java/roomescape/domain/member/Member.java b/src/main/java/roomescape/domain/member/Member.java new file mode 100644 index 0000000000..6656a27ccc --- /dev/null +++ b/src/main/java/roomescape/domain/member/Member.java @@ -0,0 +1,33 @@ +package roomescape.domain.member; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; + +@Entity +@Getter +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + protected Member() { + } + + private Member(Long id, String name) { + this.id = id; + this.name = name; + } + + public static Member of(Long id, String name) { + return new Member(id, name); + } + + public static Member createWithoutId(String name) { + return new Member(null, name); + } +} diff --git a/src/main/java/roomescape/domain/member/MemberRepository.java b/src/main/java/roomescape/domain/member/MemberRepository.java new file mode 100644 index 0000000000..4e6f1e6161 --- /dev/null +++ b/src/main/java/roomescape/domain/member/MemberRepository.java @@ -0,0 +1,6 @@ +package roomescape.domain.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { +} diff --git a/src/main/java/roomescape/domain/member/MemberService.java b/src/main/java/roomescape/domain/member/MemberService.java new file mode 100644 index 0000000000..5b24eb26ae --- /dev/null +++ b/src/main/java/roomescape/domain/member/MemberService.java @@ -0,0 +1,18 @@ +package roomescape.domain.member; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import roomescape.support.exception.MemberErrorCode; +import roomescape.support.exception.RoomescapeException; + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + + public Member findById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new RoomescapeException(MemberErrorCode.MEMBER_NOT_FOUND)); + } +} diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index f33d986412..109ab6793c 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -10,9 +10,11 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import lombok.Getter; +import roomescape.domain.member.Member; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; +import roomescape.support.exception.MemberErrorCode; import roomescape.support.exception.ReservationErrorCode; import roomescape.support.exception.ReservationTimeErrorCode; import roomescape.support.exception.RoomescapeException; @@ -28,7 +30,10 @@ public class Reservation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "date_id") @@ -43,56 +48,27 @@ public class Reservation { private Theme theme; protected Reservation() { - } - private Reservation( - Long id, - String name, - ReservationDate date, - ReservationTime time, - Theme theme - ) { - validate(name, date, time, theme); + private Reservation(Long id, Member member, ReservationDate date, ReservationTime time, Theme theme) { + validate(member, date, time, theme); this.id = id; - this.name = name; + this.member = member; this.date = date; this.time = time; this.theme = theme; } - private Reservation(String name, ReservationDate date, ReservationTime time, Theme theme) { - this(null, name, date, time, theme); + private Reservation(Member member, ReservationDate date, ReservationTime time, Theme theme) { + this(null, member, date, time, theme); } - public static Reservation createWithoutId( - String name, - ReservationDate date, - ReservationTime time, - Theme theme - ) { - return new Reservation( - name, - date, - time, - theme - ); + public static Reservation createWithoutId(Member member, ReservationDate date, ReservationTime time, Theme theme) { + return new Reservation(member, date, time, theme); } - public static Reservation of( - Long id, - String name, - ReservationDate date, - ReservationTime time, - Theme theme - ) { - return new Reservation( - id, - name, - date, - time, - theme - ); + public static Reservation of(Long id, Member member, ReservationDate date, ReservationTime time, Theme theme) { + return new Reservation(id, member, date, time, theme); } public void update(ReservationDate date, ReservationTime time) { @@ -100,9 +76,9 @@ public void update(ReservationDate date, ReservationTime time) { this.time = time; } - private static void validate(String name, ReservationDate date, ReservationTime time, Theme theme) { - if (name == null || name.isBlank()) { - throw new RoomescapeException(ReservationErrorCode.INVALID_RESERVATION_NAME); + private static void validate(Member member, ReservationDate date, ReservationTime time, Theme theme) { + if (member == null) { + throw new RoomescapeException(MemberErrorCode.INVALID_MEMBER); } if (date == null) { throw new RoomescapeException(ReservationErrorCode.INVALID_RESERVATION_DATE); diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index 6b94f3a34e..94bf623c0e 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -31,9 +31,9 @@ public ResponseEntity createReservation( return ResponseEntity.status(HttpStatus.CREATED).body(response); } - @GetMapping("/reservations") - public ResponseEntity> getReservationsByName(@RequestParam String name) { - List response = reservationService.getReservationsByName(name); + @GetMapping("/reservations-mine") + public ResponseEntity> getReservationsByMemberId(@RequestParam Long memberId) { + List response = reservationService.getReservationsByMemberId(memberId); return ResponseEntity.ok(response); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index c8fd1d39ff..de09fcec80 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -11,17 +11,17 @@ public interface ReservationRepository extends JpaRepository Long countByDateId(Long dateId); @Query(""" - select r.time.id - from Reservation r + select r.time.id + from Reservation r where r.theme.id = :themeId and r.date.id = :dateId """) List findReservedTimes(Long themeId, Long dateId); Long countByThemeId(Long id); - List findByName(String name); + List findByMemberId(Long memberId); boolean existsByDateIdAndTimeIdAndThemeId(Long dateId, Long timeId, Long themeId); - boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, Long dateId, Long timeId, Long themeId); + boolean existsByMemberIdAndDateIdAndTimeIdAndThemeId(Long memberId, Long dateId, Long timeId, Long themeId); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index d2c41c7103..0c1f38d379 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -7,6 +7,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberService; import roomescape.domain.reservation.dto.ReservationCreationRequest; import roomescape.domain.reservation.dto.ReservationCreationResponse; import roomescape.domain.reservation.dto.ReservationResponse; @@ -29,12 +31,14 @@ public class ReservationService { private final ReservationRepository reservationRepository; + private final MemberService memberService; private final ReservationDateService reservationDateService; private final ReservationTimeService reservationTimeService; private final ThemeService themeService; private final WaitingReservationRepository waitingReservationRepository; public ReservationCreationResponse createReservation(ReservationCreationRequest request) { + Member member = memberService.findById(request.memberId()); ReservationDate reservationDate = reservationDateService.findById(request.dateId()); ReservationTime reservationTime = reservationTimeService.findById(request.timeId()); validateNotPast(reservationDate, reservationTime); @@ -42,7 +46,7 @@ public ReservationCreationResponse createReservation(ReservationCreationRequest Theme theme = themeService.findById(request.themeId()); validateNotDuplicated(request.dateId(), request.timeId(), request.themeId()); Reservation savedReservation = reservationRepository.save( - request.toEntity(reservationDate, reservationTime, theme)); + request.toEntity(member, reservationDate, reservationTime, theme)); return ReservationCreationResponse.from(savedReservation); } @@ -59,8 +63,8 @@ public void deleteReservation(Long id) { } @Transactional(readOnly = true) - public List getReservationsByName(String name) { - return reservationRepository.findByName(name) + public List getReservationsByMemberId(Long memberId) { + return reservationRepository.findByMemberId(memberId) .stream() .map(ReservationResponse::from) .toList(); @@ -85,8 +89,7 @@ public ReservationResponse updateReservation(Long id, @Valid ReservationUpdateRe ReservationTime newReservationTime = reservationTimeService.findById(request.timeId()); validateNotPast(newReservationDate, newReservationTime); - validateNotDuplicated(request.dateId(), request.timeId(), reservation.getTheme() - .getId()); + validateNotDuplicated(request.dateId(), request.timeId(), reservation.getTheme().getId()); ReservationDate originalDate = reservation.getDate(); ReservationTime originalTime = reservation.getTime(); @@ -135,7 +138,7 @@ private void promoteWaitingReservationBySlot(ReservationDate date, ReservationTi if (waitingReservationOpt.isPresent()) { WaitingReservation waitingReservation = waitingReservationOpt.get(); reservationRepository.save( - Reservation.createWithoutId(waitingReservation.getName(), waitingReservation.getDate(), + Reservation.createWithoutId(waitingReservation.getMember(), waitingReservation.getDate(), waitingReservation.getTime(), waitingReservation.getTheme())); waitingReservationRepository.deleteById(waitingReservation.getId()); } diff --git a/src/main/java/roomescape/domain/reservation/dto/ReservationCreationRequest.java b/src/main/java/roomescape/domain/reservation/dto/ReservationCreationRequest.java index 8461e1f38e..e6b84a953b 100644 --- a/src/main/java/roomescape/domain/reservation/dto/ReservationCreationRequest.java +++ b/src/main/java/roomescape/domain/reservation/dto/ReservationCreationRequest.java @@ -1,15 +1,15 @@ package roomescape.domain.reservation.dto; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import roomescape.domain.member.Member; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; public record ReservationCreationRequest( - @NotBlank(message = "예약자명은 필수입니다") - String name, + @NotNull(message = "멤버 ID는 필수입니다") + Long memberId, @NotNull(message = "예약 날짜 선택은 필수입니다") Long dateId, @@ -21,12 +21,8 @@ public record ReservationCreationRequest( Long themeId ) { - public Reservation toEntity(ReservationDate reservationDate, ReservationTime reservationTime, Theme theme) { - return Reservation.createWithoutId( - name, - reservationDate, - reservationTime, - theme - ); + public Reservation toEntity(Member member, ReservationDate reservationDate, ReservationTime reservationTime, + Theme theme) { + return Reservation.createWithoutId(member, reservationDate, reservationTime, theme); } } diff --git a/src/main/java/roomescape/domain/reservation/dto/ReservationCreationResponse.java b/src/main/java/roomescape/domain/reservation/dto/ReservationCreationResponse.java index 446d2f3acf..9704f93d15 100644 --- a/src/main/java/roomescape/domain/reservation/dto/ReservationCreationResponse.java +++ b/src/main/java/roomescape/domain/reservation/dto/ReservationCreationResponse.java @@ -18,7 +18,7 @@ public record ReservationCreationResponse( public static ReservationCreationResponse from(Reservation reservation) { return new ReservationCreationResponse( reservation.getId(), - reservation.getName(), + reservation.getMember().getName(), reservation.getDate().getPlayDay(), reservation.getTime().getStartAt(), ThemePayload.from(reservation.getTheme()) diff --git a/src/main/java/roomescape/domain/reservation/dto/ReservationResponse.java b/src/main/java/roomescape/domain/reservation/dto/ReservationResponse.java index 9628136467..72d45d5f96 100644 --- a/src/main/java/roomescape/domain/reservation/dto/ReservationResponse.java +++ b/src/main/java/roomescape/domain/reservation/dto/ReservationResponse.java @@ -18,7 +18,7 @@ public record ReservationResponse( public static ReservationResponse from(Reservation reservation) { return new ReservationResponse( reservation.getId(), - reservation.getName(), + reservation.getMember().getName(), reservation.getDate().getPlayDay(), ReservationTimePayload.from(reservation.getTime()), ThemePayload.from(reservation.getTheme()) diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservation.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservation.java index 02d631c84b..4e14aa29f1 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservation.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservation.java @@ -11,15 +11,17 @@ import jakarta.persistence.UniqueConstraint; import java.time.LocalDateTime; import lombok.Getter; +import roomescape.domain.member.Member; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; +import roomescape.support.exception.MemberErrorCode; import roomescape.support.exception.RoomescapeException; import roomescape.support.exception.WaitingReservationErrorCode; @Entity @Table(uniqueConstraints = { - @UniqueConstraint(columnNames = {"name", "date_id", "time_id", "theme_id"}) + @UniqueConstraint(columnNames = {"member_id", "date_id", "time_id", "theme_id"}) }) @Getter public class WaitingReservation { @@ -27,7 +29,10 @@ public class WaitingReservation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "date_id") @@ -44,35 +49,33 @@ public class WaitingReservation { private LocalDateTime createdAt; protected WaitingReservation() { - } - private WaitingReservation(Long id, String name, ReservationDate date, ReservationTime time, Theme theme, + private WaitingReservation(Long id, Member member, ReservationDate date, ReservationTime time, Theme theme, LocalDateTime createdAt) { - validate(name, createdAt); + validate(member, createdAt); this.id = id; - this.name = name; + this.member = member; this.date = date; this.time = time; this.theme = theme; this.createdAt = createdAt; } - public static WaitingReservation createWithoutId(String name, ReservationDate date, ReservationTime time, + public static WaitingReservation createWithoutId(Member member, ReservationDate date, ReservationTime time, Theme theme, LocalDateTime createdAt) { - return new WaitingReservation(null, name, date, time, theme, createdAt); + return new WaitingReservation(null, member, date, time, theme, createdAt); } - public static WaitingReservation of(Long id, String name, ReservationDate date, ReservationTime time, Theme theme, - LocalDateTime createdAt) { - return new WaitingReservation(id, name, date, time, theme, createdAt); + public static WaitingReservation of(Long id, Member member, ReservationDate date, ReservationTime time, + Theme theme, LocalDateTime createdAt) { + return new WaitingReservation(id, member, date, time, theme, createdAt); } - private static void validate(String name, LocalDateTime createdAt) { - if (name == null || name.isBlank()) { - throw new RoomescapeException(WaitingReservationErrorCode.INVALID_RESERVATION_NAME); + private static void validate(Member member, LocalDateTime createdAt) { + if (member == null) { + throw new RoomescapeException(MemberErrorCode.INVALID_MEMBER); } - if (createdAt == null) { throw new RoomescapeException(WaitingReservationErrorCode.INVALID_CREATED_AT); } diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationController.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationController.java index a3ca45e864..caebe3b401 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationController.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationController.java @@ -25,7 +25,8 @@ public class WaitingReservationController { private final WaitingReservationService waitingReservationService; @PostMapping - public ResponseEntity createWaitingReservation(@Valid @RequestBody WaitingReservationCreationRequest request) { + public ResponseEntity createWaitingReservation( + @Valid @RequestBody WaitingReservationCreationRequest request) { WaitingReservationCreationResponse response = waitingReservationService.createWaitingReservation(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } @@ -37,8 +38,10 @@ public ResponseEntity cancelWaitingReservation(@PathVariable Long id) { } @GetMapping - public ResponseEntity> getWaitingReservations(@RequestParam String name) { - List response = waitingReservationService.getWaitingReservationsWithRankByName(name); + public ResponseEntity> getWaitingReservations( + @RequestParam Long memberId) { + List response = + waitingReservationService.getWaitingReservationsWithRankByMemberId(memberId); return ResponseEntity.ok(response); } } diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationRepository.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationRepository.java index 10826dad47..2331e25526 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationRepository.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationRepository.java @@ -7,11 +7,10 @@ import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import roomescape.domain.waitingreservation.dto.RankProjection; -import roomescape.domain.waitingreservation.dto.WaitingReservationWithRank; public interface WaitingReservationRepository extends JpaRepository { - boolean existsByNameAndDateIdAndTimeIdAndThemeId(String name, Long dateId, Long timeId, Long themeId); + boolean existsByMemberIdAndDateIdAndTimeIdAndThemeId(Long memberId, Long dateId, Long timeId, Long themeId); @Lock(LockModeType.PESSIMISTIC_WRITE) @Query(""" @@ -26,7 +25,7 @@ public interface WaitingReservationRepository extends JpaRepository findOldestBySlot(Long dateId, Long timeId, Long themeId); - List findAllByName(String name); + List findAllByMemberId(Long memberId); @Query(value = """ select wr.id, row_number() over ( @@ -37,8 +36,8 @@ select wr.id, row_number() over ( join ( select date_id, time_id, theme_id from waiting_reservation - where name = :name + where member_id = :memberId ) slot on wr.date_id = slot.date_id and wr.time_id = slot.time_id and wr.theme_id = slot.theme_id """, nativeQuery = true) - List findRankByName(String name); + List findRankByMemberId(Long memberId); } diff --git a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java index c1a69d5358..e97ecbb033 100644 --- a/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java +++ b/src/main/java/roomescape/domain/waitingreservation/WaitingReservationService.java @@ -7,6 +7,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberService; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateService; @@ -30,11 +32,13 @@ public class WaitingReservationService { private final WaitingReservationRepository waitingReservationRepository; private final ReservationRepository reservationRepository; + private final MemberService memberService; private final ReservationDateService reservationDateService; private final ReservationTimeService reservationTimeService; private final ThemeService themeService; public WaitingReservationCreationResponse createWaitingReservation(WaitingReservationCreationRequest request) { + Member member = memberService.findById(request.memberId()); ReservationDate date = reservationDateService.findById(request.dateId()); ReservationTime time = reservationTimeService.findById(request.timeId()); Theme theme = themeService.findById(request.themeId()); @@ -43,19 +47,18 @@ public WaitingReservationCreationResponse createWaitingReservation(WaitingReserv validateAlreadyReserved(request); validateDuplicationOfWaitingReservation(request); - WaitingReservation waitingReservation = request.toEntity(date, time, theme, LocalDateTime.now()); + WaitingReservation waitingReservation = request.toEntity(member, date, time, theme, LocalDateTime.now()); WaitingReservation savedWaitingReservation = waitingReservationRepository.save(waitingReservation); return WaitingReservationCreationResponse.from(savedWaitingReservation); } public void cancelWaitingReservation(Long id) { - WaitingReservation waitingReservation = waitingReservationRepository .findById(id) .orElseThrow(() -> new RoomescapeException(WaitingReservationErrorCode.WAITING_RESERVATION_NOT_FOUND)); - if (reservationRepository.existsByNameAndDateIdAndTimeIdAndThemeId( - waitingReservation.getName(), + if (reservationRepository.existsByMemberIdAndDateIdAndTimeIdAndThemeId( + waitingReservation.getMember().getId(), waitingReservation.getDate().getId(), waitingReservation.getTime().getId(), waitingReservation.getTheme().getId())) { @@ -65,10 +68,10 @@ public void cancelWaitingReservation(Long id) { waitingReservationRepository.deleteById(id); } - public List getWaitingReservationsWithRankByName(String name) { - List waitingReservations = waitingReservationRepository.findAllByName(name); + public List getWaitingReservationsWithRankByMemberId(Long memberId) { + List waitingReservations = waitingReservationRepository.findAllByMemberId(memberId); - Map rankMap = waitingReservationRepository.findRankByName(name) + Map rankMap = waitingReservationRepository.findRankByMemberId(memberId) .stream() .collect(Collectors.toMap(RankProjection::getId, RankProjection::getRank)); @@ -85,8 +88,8 @@ private void validateNotPast(ReservationDate reservationDate, ReservationTime re } private void validateAlreadyReserved(WaitingReservationCreationRequest request) { - if(reservationRepository.existsByNameAndDateIdAndTimeIdAndThemeId( - request.name(), + if (reservationRepository.existsByMemberIdAndDateIdAndTimeIdAndThemeId( + request.memberId(), request.dateId(), request.timeId(), request.themeId())) { @@ -95,8 +98,8 @@ private void validateAlreadyReserved(WaitingReservationCreationRequest request) } private void validateDuplicationOfWaitingReservation(WaitingReservationCreationRequest request) { - if (waitingReservationRepository.existsByNameAndDateIdAndTimeIdAndThemeId( - request.name(), + if (waitingReservationRepository.existsByMemberIdAndDateIdAndTimeIdAndThemeId( + request.memberId(), request.dateId(), request.timeId(), request.themeId())) { diff --git a/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationCreationRequest.java b/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationCreationRequest.java index cbc076bf60..a28237fa62 100644 --- a/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationCreationRequest.java +++ b/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationCreationRequest.java @@ -1,16 +1,16 @@ package roomescape.domain.waitingreservation.dto; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; +import roomescape.domain.member.Member; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; import roomescape.domain.waitingreservation.WaitingReservation; public record WaitingReservationCreationRequest( - @NotBlank(message = "예약자명은 필수입니다") - String name, + @NotNull(message = "멤버 ID는 필수입니다") + Long memberId, @NotNull(message = "예약 날짜 선택은 필수입니다") Long dateId, @@ -23,11 +23,12 @@ public record WaitingReservationCreationRequest( ) { public WaitingReservation toEntity( + Member member, ReservationDate date, ReservationTime time, Theme theme, LocalDateTime createdAt ) { - return WaitingReservation.createWithoutId(name, date, time, theme, createdAt); + return WaitingReservation.createWithoutId(member, date, time, theme, createdAt); } } diff --git a/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationCreationResponse.java b/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationCreationResponse.java index 20bc0ae02c..38c772ab54 100644 --- a/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationCreationResponse.java +++ b/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationCreationResponse.java @@ -20,7 +20,7 @@ public record WaitingReservationCreationResponse( public static WaitingReservationCreationResponse from(WaitingReservation waitingReservation) { return new WaitingReservationCreationResponse( waitingReservation.getId(), - waitingReservation.getName(), + waitingReservation.getMember().getName(), waitingReservation.getDate().getPlayDay(), waitingReservation.getTime().getStartAt(), ThemePayload.from(waitingReservation.getTheme()), diff --git a/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationWithRankResponse.java b/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationWithRankResponse.java index 212ac93989..d41cff1783 100644 --- a/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationWithRankResponse.java +++ b/src/main/java/roomescape/domain/waitingreservation/dto/WaitingReservationWithRankResponse.java @@ -18,7 +18,7 @@ public static WaitingReservationWithRankResponse from(WaitingReservationWithRank WaitingReservation waitingReservation = waitingReservationWithRank.waitingReservation(); return new WaitingReservationWithRankResponse( waitingReservation.getId(), - waitingReservation.getName(), + waitingReservation.getMember().getName(), waitingReservation.getDate().getPlayDay(), ReservationResponse.ReservationTimePayload.from(waitingReservation.getTime()), ReservationResponse.ThemePayload.from(waitingReservation.getTheme()), diff --git a/src/main/java/roomescape/support/exception/MemberErrorCode.java b/src/main/java/roomescape/support/exception/MemberErrorCode.java new file mode 100644 index 0000000000..03eb2f5247 --- /dev/null +++ b/src/main/java/roomescape/support/exception/MemberErrorCode.java @@ -0,0 +1,27 @@ +package roomescape.support.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum MemberErrorCode implements ErrorCode { + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, + "지정한 식별자에 해당하는 멤버를 찾을 수 없습니다.", "요청한 멤버 ID의 유효성 및 DB 존재 여부를 확인하십시오."), + INVALID_MEMBER(HttpStatus.BAD_REQUEST, + "멤버 정보가 유효하지 않습니다.", "memberId 필드 포함 여부 및 데이터 형식을 확인하십시오."); + + private final HttpStatus httpStatus; + private final String message; + private final String action; + + MemberErrorCode(HttpStatus httpStatus, String message, String action) { + this.httpStatus = httpStatus; + this.message = message; + this.action = action; + } + + @Override + public String getCode() { + return name(); + } +} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 373a25f1eb..6ccf648a4b 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -32,44 +32,74 @@ VALUES ('공포', '오금이 저리는 공포입니다.', '/themes/scary'), ('코미디', '유쾌한 소동이 가득한 코미디 테마입니다.', '/themes/comedy'), ('느와르', '어두운 도시를 배경으로 한 느와르 테마입니다.', '/themes/noir'); -INSERT INTO reservation (name, date_id, time_id, theme_id) -VALUES ('보예', 1, 1, 1), - ('이산', 1, 2, 1), - ('나무', 2, 1, 2), - ('피즈', 2, 3, 2), - ('제이콥', 3, 1, 1), - ('보예짱', 3, 4, 3), - ('이산짱', 3, 2, 1), - ('나무짱', 4, 3, 3), - ('피즈짱', 4, 1, 2), - ('샤를', 4, 4, 1), - ('마이찬', 5, 2, 1), - ('샤를짱', 5, 3, 8), - ('마이찬짱', 5, 4, 5), - ('브라운', 6, 2, 11), - ('네오', 6, 4, 4), - ('브리', 6, 1, 9), - ('구구', 7, 3, 6), - ('리사', 7, 1, 12), - ('레서', 7, 4, 7), - ('바니', 8, 2, 10), - ('소낙눈', 8, 3, 3), - ('카야', 8, 4, 8), - ('피노', 9, 1, 5), - ('우디', 9, 2, 11), - ('캐모', 10, 3, 4), - ('아이큐', 10, 1, 9), - ('쿠다', 11, 3, 6), - ('고래', 11, 4, 10); +INSERT INTO member (name) +VALUES ('보예'), -- 1 + ('이산'), -- 2 + ('나무'), -- 3 + ('피즈'), -- 4 + ('제이콥'), -- 5 + ('보예짱'), -- 6 + ('이산짱'), -- 7 + ('나무짱'), -- 8 + ('피즈짱'), -- 9 + ('샤를'), -- 10 + ('마이찬'), -- 11 + ('샤를짱'), -- 12 + ('마이찬짱'), -- 13 + ('브라운'), -- 14 + ('네오'), -- 15 + ('브리'), -- 16 + ('구구'), -- 17 + ('리사'), -- 18 + ('레서'), -- 19 + ('바니'), -- 20 + ('소낙눈'), -- 21 + ('카야'), -- 22 + ('피노'), -- 23 + ('우디'), -- 24 + ('캐모'), -- 25 + ('아이큐'), -- 26 + ('쿠다'), -- 27 + ('고래'); -- 28 --- 고래 대기 데이터 --- 슬롯1 (date+1, time2, theme1): 이산이 먼저 대기 → 고래 2순위 +INSERT INTO reservation (member_id, date_id, time_id, theme_id) +VALUES (1, 1, 1, 1), + (2, 1, 2, 1), + (3, 2, 1, 2), + (4, 2, 3, 2), + (5, 3, 1, 1), + (6, 3, 4, 3), + (7, 3, 2, 1), + (8, 4, 3, 3), + (9, 4, 1, 2), + (10, 4, 4, 1), + (11, 5, 2, 1), + (12, 5, 3, 8), + (13, 5, 4, 5), + (14, 6, 2, 11), + (15, 6, 4, 4), + (16, 6, 1, 9), + (17, 7, 3, 6), + (18, 7, 1, 12), + (19, 7, 4, 7), + (20, 8, 2, 10), + (21, 8, 3, 3), + (22, 8, 4, 8), + (23, 9, 1, 5), + (24, 9, 2, 11), + (25, 10, 3, 4), + (26, 10, 1, 9), + (27, 11, 3, 6), + (28, 11, 4, 10); + +-- 고래(28) 대기 데이터 +-- 슬롯1 (date+1, time2, theme1): 이산(2)이 먼저 대기 → 고래 2순위 -- 슬롯2 (date+2, time2, theme11): 고래만 대기 → 고래 1순위 --- 슬롯3 (date+3, time3, theme6): 보예·나무가 먼저 대기 → 고래 3순위 -INSERT INTO waiting_reservation (name, date_id, time_id, theme_id, created_at) -VALUES ('이산', 5, 2, 1, CURRENT_TIMESTAMP - INTERVAL '3' HOUR), - ('고래', 5, 2, 1, CURRENT_TIMESTAMP - INTERVAL '2' HOUR), - ('고래', 6, 2, 11, CURRENT_TIMESTAMP - INTERVAL '2' HOUR), - ('보예', 7, 3, 6, CURRENT_TIMESTAMP - INTERVAL '4' HOUR), - ('나무', 7, 3, 6, CURRENT_TIMESTAMP - INTERVAL '3' HOUR), - ('고래', 7, 3, 6, CURRENT_TIMESTAMP - INTERVAL '2' HOUR); +-- 슬롯3 (date+3, time3, theme6): 보예(1)·나무(3)가 먼저 대기 → 고래 3순위 +INSERT INTO waiting_reservation (member_id, date_id, time_id, theme_id, created_at) +VALUES (2, 5, 2, 1, CURRENT_TIMESTAMP - INTERVAL '3' HOUR), + (28, 5, 2, 1, CURRENT_TIMESTAMP - INTERVAL '2' HOUR), + (28, 6, 2, 11, CURRENT_TIMESTAMP - INTERVAL '2' HOUR), + (1, 7, 3, 6, CURRENT_TIMESTAMP - INTERVAL '4' HOUR), + (3, 7, 3, 6, CURRENT_TIMESTAMP - INTERVAL '3' HOUR), + (28, 7, 3, 6, CURRENT_TIMESTAMP - INTERVAL '2' HOUR); diff --git a/src/test/java/roomescape/domain/reservation/AdminReservationControllerTest.java b/src/test/java/roomescape/domain/reservation/AdminReservationControllerTest.java index 11c20e4f14..d5900a5a1f 100644 --- a/src/test/java/roomescape/domain/reservation/AdminReservationControllerTest.java +++ b/src/test/java/roomescape/domain/reservation/AdminReservationControllerTest.java @@ -13,6 +13,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; import roomescape.domain.reservationtime.ReservationTime; @@ -44,6 +46,9 @@ class AdminReservationControllerTest { @Autowired private ThemeRepository themeRepository; + @Autowired + private MemberRepository memberRepository; + private ReservationDate futureDate; private ReservationTime time; private Theme theme; @@ -59,7 +64,8 @@ void setUp() { @Test @DisplayName("관리자 권한으로 모든 예약을 조회한다.") void getAllReservations() { - reservationRepository.save(Reservation.createWithoutId("관리자조회용", futureDate, time, theme)); + Member member = memberRepository.save(Member.createWithoutId("관리자조회용")); + reservationRepository.save(Reservation.createWithoutId(member, futureDate, time, theme)); RestAssured.given().log().all() .header(ADMIN_HEADER, adminToken) @@ -72,7 +78,8 @@ void getAllReservations() { @Test @DisplayName("관리자 권한으로 예약을 삭제한다.") void deleteReservation() { - Reservation saved = reservationRepository.save(Reservation.createWithoutId("삭제될예약", futureDate, time, theme)); + Member member = memberRepository.save(Member.createWithoutId("삭제될예약")); + Reservation saved = reservationRepository.save(Reservation.createWithoutId(member, futureDate, time, theme)); RestAssured.given().log().all() .header(ADMIN_HEADER, adminToken) diff --git a/src/test/java/roomescape/domain/reservation/ReservationCancellationIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationCancellationIntegrationTest.java index 1e6f18a875..5fd3dc363e 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationCancellationIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationCancellationIntegrationTest.java @@ -14,6 +14,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; import roomescape.domain.reservationtime.ReservationTime; @@ -36,6 +38,9 @@ class ReservationCancellationIntegrationTest { @MockitoSpyBean private WaitingReservationRepository waitingReservationRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired private ReservationDateRepository reservationDateRepository; @@ -54,30 +59,30 @@ void setUp() { @Test void 사용자가_본인의_예약을_취소하면_같은_슬롯의_1순위_대기가_예약으로_변경된다() { + Member tester = memberRepository.save(Member.createWithoutId("테스터")); + Member san = memberRepository.save(Member.createWithoutId("이산")); + Member whale = memberRepository.save(Member.createWithoutId("고래")); + Member otherMember = memberRepository.save(Member.createWithoutId("다른슬롯")); + Reservation cancelledReservation = reservationRepository.save( - Reservation.createWithoutId( - "테스터", - cancelledSlot.date(), - cancelledSlot.time(), - cancelledSlot.theme() - ) + Reservation.createWithoutId(tester, cancelledSlot.date(), cancelledSlot.time(), cancelledSlot.theme()) ); Slot otherSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(11, 0), "스릴러"); WaitingReservation otherSlotOldest = waitingReservationRepository.save( - waiting("다른슬롯", otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) + waiting(otherMember, otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) ); WaitingReservation firstWaiting = waitingReservationRepository.save( - waiting("이산", cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + waiting(san, cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) ); WaitingReservation secondWaiting = waitingReservationRepository.save( - waiting("고래", cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + waiting(whale, cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) ); reservationService.cancelReservation(cancelledReservation.getId()); assertThat(reservationRepository.findById(cancelledReservation.getId())).isEmpty(); - assertThat(reservationRepository.findByName("이산")).hasSize(1); - assertThat(reservationRepository.findByName("다른슬롯")).isEmpty(); + assertThat(reservationRepository.findByMemberId(san.getId())).hasSize(1); + assertThat(reservationRepository.findByMemberId(otherMember.getId())).isEmpty(); assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isEmpty(); assertThat(waitingReservationRepository.findById(secondWaiting.getId())).isPresent(); assertThat(waitingReservationRepository.findById(otherSlotOldest.getId())).isPresent(); @@ -85,23 +90,23 @@ void setUp() { @Test void 예약_취소_중_1순위_예약_대기_추가가_실패하면_전체가_롤백된다() { + Member tester = memberRepository.save(Member.createWithoutId("테스터")); + Member san = memberRepository.save(Member.createWithoutId("이산")); + Member whale = memberRepository.save(Member.createWithoutId("고래")); + Member otherMember = memberRepository.save(Member.createWithoutId("다른슬롯")); + Reservation cancelledReservation = reservationRepository.save( - Reservation.createWithoutId( - "테스터", - cancelledSlot.date(), - cancelledSlot.time(), - cancelledSlot.theme() - ) + Reservation.createWithoutId(tester, cancelledSlot.date(), cancelledSlot.time(), cancelledSlot.theme()) ); Slot otherSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(11, 0), "스릴러"); waitingReservationRepository.save( - waiting("다른슬롯", otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) + waiting(otherMember, otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) ); WaitingReservation firstWaiting = waitingReservationRepository.save( - waiting("이산", cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + waiting(san, cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) ); waitingReservationRepository.save( - waiting("고래", cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + waiting(whale, cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) ); doThrow(new RuntimeException()) @@ -113,28 +118,28 @@ void setUp() { assertThat(reservationRepository.findById(cancelledReservation.getId())).isPresent(); assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); - assertThat(reservationRepository.findByName("이산")).isEmpty(); + assertThat(reservationRepository.findByMemberId(san.getId())).isEmpty(); } @Test void 예약_취소_중_1순위_예약_대기_삭제가_실패하면_전체가_롤백된다() { + Member tester = memberRepository.save(Member.createWithoutId("테스터")); + Member san = memberRepository.save(Member.createWithoutId("이산")); + Member whale = memberRepository.save(Member.createWithoutId("고래")); + Member otherMember = memberRepository.save(Member.createWithoutId("다른슬롯")); + Reservation cancelledReservation = reservationRepository.save( - Reservation.createWithoutId( - "테스터", - cancelledSlot.date(), - cancelledSlot.time(), - cancelledSlot.theme() - ) + Reservation.createWithoutId(tester, cancelledSlot.date(), cancelledSlot.time(), cancelledSlot.theme()) ); Slot otherSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(11, 0), "스릴러"); waitingReservationRepository.save( - waiting("다른슬롯", otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) + waiting(otherMember, otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) ); WaitingReservation firstWaiting = waitingReservationRepository.save( - waiting("이산", cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + waiting(san, cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) ); waitingReservationRepository.save( - waiting("고래", cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + waiting(whale, cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) ); doThrow(new RuntimeException()).when(waitingReservationRepository).deleteById(firstWaiting.getId()); @@ -144,28 +149,28 @@ void setUp() { assertThat(reservationRepository.findById(cancelledReservation.getId())).isPresent(); assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); - assertThat(reservationRepository.findByName("이산")).isEmpty(); + assertThat(reservationRepository.findByMemberId(san.getId())).isEmpty(); } @Test void 예약_취소_중_기존_예약_삭제가_실패하면_전체가_롤백된다() { + Member tester = memberRepository.save(Member.createWithoutId("테스터")); + Member san = memberRepository.save(Member.createWithoutId("이산")); + Member whale = memberRepository.save(Member.createWithoutId("고래")); + Member otherMember = memberRepository.save(Member.createWithoutId("다른슬롯")); + Reservation cancelledReservation = reservationRepository.save( - Reservation.createWithoutId( - "테스터", - cancelledSlot.date(), - cancelledSlot.time(), - cancelledSlot.theme() - ) + Reservation.createWithoutId(tester, cancelledSlot.date(), cancelledSlot.time(), cancelledSlot.theme()) ); Slot otherSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(11, 0), "스릴러"); waitingReservationRepository.save( - waiting("다른슬롯", otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) + waiting(otherMember, otherSlot, LocalDateTime.of(2026, 5, 5, 10, 0)) ); WaitingReservation firstWaiting = waitingReservationRepository.save( - waiting("이산", cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + waiting(san, cancelledSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) ); waitingReservationRepository.save( - waiting("고래", cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + waiting(whale, cancelledSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) ); doThrow(new RuntimeException()).when(reservationRepository).deleteById(cancelledReservation.getId()); @@ -175,11 +180,11 @@ void setUp() { assertThat(reservationRepository.findById(cancelledReservation.getId())).isPresent(); assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); - assertThat(reservationRepository.findByName("이산")).isEmpty(); + assertThat(reservationRepository.findByMemberId(san.getId())).isEmpty(); } - private WaitingReservation waiting(String name, Slot slot, LocalDateTime createdAt) { - return WaitingReservation.createWithoutId(name, slot.date(), slot.time(), slot.theme(), createdAt); + private WaitingReservation waiting(Member member, Slot slot, LocalDateTime createdAt) { + return WaitingReservation.createWithoutId(member, slot.date(), slot.time(), slot.theme(), createdAt); } private Slot insertSlot(LocalDate playDay, LocalTime startAt, String themeName) { @@ -189,10 +194,6 @@ private Slot insertSlot(LocalDate playDay, LocalTime startAt, String themeName) return new Slot(date, time, theme); } - private record Slot( - ReservationDate date, - ReservationTime time, - Theme theme - ) { + private record Slot(ReservationDate date, ReservationTime time, Theme theme) { } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java b/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java index 55108d5487..4a88b34577 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java @@ -15,6 +15,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; import roomescape.domain.reservationtime.ReservationTime; @@ -32,6 +34,9 @@ class ReservationControllerTest { @Autowired private ReservationRepository reservationRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired private ReservationDateRepository reservationDateRepository; @@ -46,6 +51,7 @@ class ReservationControllerTest { private ReservationDate todayDate; private ReservationTime time; private Theme theme; + private Member member; @BeforeEach void setUp() { @@ -55,169 +61,172 @@ void setUp() { todayDate = reservationDateRepository.save(ReservationDate.createWithoutId(LocalDate.now())); time = reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(22, 0))); theme = themeRepository.save(Theme.createWithoutId("테스트테마", "설명", "url")); + member = memberRepository.save(Member.createWithoutId("테스터")); } @Test @DisplayName("예약을 생성한다.") void createReservation() { Map params = new HashMap<>(); - params.put("name", "테스터"); + params.put("memberId", member.getId()); params.put("dateId", futureDate.getId()); params.put("timeId", time.getId()); params.put("themeId", theme.getId()); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().post("/reservations") - .then().log().all() - .statusCode(201) - .body("name", is("테스터")); + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(201) + .body("name", is("테스터")); } @Test @DisplayName("과거 시간으로 예약을 생성할 수 없다.") void createReservation_Fail_PastTime() { Map params = new HashMap<>(); - params.put("name", "테스터"); + params.put("memberId", member.getId()); params.put("dateId", pastDate.getId()); params.put("timeId", time.getId()); params.put("themeId", theme.getId()); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().post("/reservations") - .then().log().all() - .statusCode(400); + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(400); } @Test @DisplayName("중복된 예약은 생성할 수 없다.") void createReservation_Fail_Duplicated() { - reservationRepository.save(Reservation.createWithoutId("기존테스터", futureDate, time, theme)); + Member existing = memberRepository.save(Member.createWithoutId("기존테스터")); + reservationRepository.save(Reservation.createWithoutId(existing, futureDate, time, theme)); Map params = new HashMap<>(); - params.put("name", "새로운테스터"); + params.put("memberId", member.getId()); params.put("dateId", futureDate.getId()); params.put("timeId", time.getId()); params.put("themeId", theme.getId()); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().post("/reservations") - .then().log().all() - .statusCode(409); + .contentType(ContentType.JSON) + .body(params) + .when().post("/reservations") + .then().log().all() + .statusCode(409); } @Test - @DisplayName("이름으로 예약을 조회한다.") - void getReservationsByName() { - reservationRepository.save(Reservation.createWithoutId("테스터", futureDate, time, theme)); + @DisplayName("멤버 ID로 예약을 조회한다.") + void getReservationsByMemberId() { + reservationRepository.save(Reservation.createWithoutId(member, futureDate, time, theme)); RestAssured.given().log().all() - .param("name", "테스터") - .when().get("/reservations") - .then().log().all() - .statusCode(200) - .body("any { it.name == '테스터' }", is(true)); + .param("memberId", member.getId()) + .when().get("/reservations-mine") + .then().log().all() + .statusCode(200) + .body("any { it.name == '테스터' }", is(true)); } @Test @DisplayName("예약을 취소(삭제)한다.") void cancelReservation() { - Reservation saved = reservationRepository.save(Reservation.createWithoutId("취소테스터", futureDate, time, theme)); + Reservation saved = reservationRepository.save(Reservation.createWithoutId(member, futureDate, time, theme)); RestAssured.given().log().all() - .when().delete("/reservations/" + saved.getId()) - .then().log().all() - .statusCode(204); + .when().delete("/reservations/" + saved.getId()) + .then().log().all() + .statusCode(204); } @Test @DisplayName("당일 예약은 취소(삭제)할 수 없다.") void cancelReservation_Fail_Today() { - Reservation saved = reservationRepository.save(Reservation.createWithoutId("취소불가테스터", todayDate, time, theme)); + Reservation saved = reservationRepository.save(Reservation.createWithoutId(member, todayDate, time, theme)); RestAssured.given().log().all() - .when().delete("/reservations/" + saved.getId()) - .then().log().all() - .statusCode(400); + .when().delete("/reservations/" + saved.getId()) + .then().log().all() + .statusCode(400); } @Test @DisplayName("예약을 수정한다.") void updateReservation() { - Reservation saved = reservationRepository.save(Reservation.createWithoutId("수정테스터", futureDate, time, theme)); + Reservation saved = reservationRepository.save(Reservation.createWithoutId(member, futureDate, time, theme)); ReservationDate newDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.now().plusDays(15))); + ReservationDate.createWithoutId(LocalDate.now().plusDays(15))); Map params = new HashMap<>(); params.put("dateId", newDate.getId()); params.put("timeId", time.getId()); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().patch("/reservations/" + saved.getId()) - .then().log().all() - .statusCode(200); + .contentType(ContentType.JSON) + .body(params) + .when().patch("/reservations/" + saved.getId()) + .then().log().all() + .statusCode(200); } @Test @DisplayName("당일 예약은 수정할 수 없다.") void updateReservation_Fail_Today() { - Reservation saved = reservationRepository.save(Reservation.createWithoutId("당일예약테스터", todayDate, time, theme)); + Reservation saved = reservationRepository.save(Reservation.createWithoutId(member, todayDate, time, theme)); Map params = new HashMap<>(); params.put("dateId", futureDate.getId()); params.put("timeId", time.getId()); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().patch("/reservations/" + saved.getId()) - .then().log().all() - .statusCode(400); + .contentType(ContentType.JSON) + .body(params) + .when().patch("/reservations/" + saved.getId()) + .then().log().all() + .statusCode(400); } @Test @DisplayName("과거 시간으로 예약을 수정할 수 없다.") void updateReservation_Fail_PastTime() { - Reservation saved = reservationRepository.save(Reservation.createWithoutId("수정테스터", futureDate, time, theme)); + Reservation saved = reservationRepository.save(Reservation.createWithoutId(member, futureDate, time, theme)); Map params = new HashMap<>(); params.put("dateId", pastDate.getId()); params.put("timeId", time.getId()); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().patch("/reservations/" + saved.getId()) - .then().log().all() - .statusCode(400); + .contentType(ContentType.JSON) + .body(params) + .when().patch("/reservations/" + saved.getId()) + .then().log().all() + .statusCode(400); } @Test @DisplayName("수정하려는 시간에 이미 다른 예약이 있으면 수정할 수 없다.") void updateReservation_Fail_Duplicated() { Reservation myReservation = reservationRepository.save( - Reservation.createWithoutId("내예약", futureDate, time, theme)); + Reservation.createWithoutId(member, futureDate, time, theme)); + Member other = memberRepository.save(Member.createWithoutId("다른사람")); ReservationDate otherDate = reservationDateRepository.save( - ReservationDate.createWithoutId(LocalDate.now().plusDays(15))); - reservationRepository.save(Reservation.createWithoutId("다른사람예약", otherDate, time, theme)); + ReservationDate.createWithoutId(LocalDate.now().plusDays(15))); + reservationRepository.save(Reservation.createWithoutId(other, otherDate, time, theme)); Map params = new HashMap<>(); params.put("dateId", otherDate.getId()); params.put("timeId", time.getId()); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().patch("/reservations/" + myReservation.getId()) - .then().log().all() - .statusCode(409); + .contentType(ContentType.JSON) + .body(params) + .when().patch("/reservations/" + myReservation.getId()) + .then().log().all() + .statusCode(409); } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index 4a9b567a51..3d833cd4e7 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -11,6 +11,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.dto.ReservationCreationRequest; import roomescape.domain.reservation.dto.ReservationCreationResponse; import roomescape.domain.reservation.dto.ReservationResponse; @@ -35,6 +37,9 @@ class ReservationServiceTest { @Autowired private ReservationRepository reservationRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired private ReservationDateRepository reservationDateRepository; @@ -47,12 +52,13 @@ class ReservationServiceTest { @Test @DisplayName("예약을 생성한다.") void createReservation() { + Member member = createMember("테스터"); ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); Theme theme = createTheme("테마"); ReservationCreationResponse response = reservationService.createReservation( - new ReservationCreationRequest("테스터", date.getId(), time.getId(), theme.getId())); + new ReservationCreationRequest(member.getId(), date.getId(), time.getId(), theme.getId())); assertThat(response.name()).isEqualTo("테스터"); } @@ -60,41 +66,45 @@ void createReservation() { @Test @DisplayName("과거 시간으로 예약 생성 시 예외가 발생한다.") void createReservationWithPastTime() { + Member member = createMember("테스터"); ReservationDate date = createDate(LocalDate.now().minusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); Theme theme = createTheme("테마"); assertThatThrownBy(() -> reservationService.createReservation( - new ReservationCreationRequest("테스터", date.getId(), time.getId(), theme.getId()))) - .isInstanceOf(RoomescapeException.class) - .hasMessageContaining(ReservationDateErrorCode.PAST_DATE_NOT_ALLOWED.getMessage()); + new ReservationCreationRequest(member.getId(), date.getId(), time.getId(), theme.getId()))) + .isInstanceOf(RoomescapeException.class) + .hasMessageContaining(ReservationDateErrorCode.PAST_DATE_NOT_ALLOWED.getMessage()); } @Test @DisplayName("중복된 예약 생성 시 예외가 발생한다.") void createDuplicateReservation() { + Member member1 = createMember("테스터1"); + Member member2 = createMember("테스터2"); ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); Theme theme = createTheme("테마"); reservationService.createReservation( - new ReservationCreationRequest("테스터1", date.getId(), time.getId(), theme.getId())); + new ReservationCreationRequest(member1.getId(), date.getId(), time.getId(), theme.getId())); assertThatThrownBy(() -> reservationService.createReservation( - new ReservationCreationRequest("테스터2", date.getId(), time.getId(), theme.getId()))) - .isInstanceOf(RoomescapeException.class) - .hasMessageContaining(ReservationErrorCode.RESERVATION_DUPLICATED.getMessage()); + new ReservationCreationRequest(member2.getId(), date.getId(), time.getId(), theme.getId()))) + .isInstanceOf(RoomescapeException.class) + .hasMessageContaining(ReservationErrorCode.RESERVATION_DUPLICATED.getMessage()); } @Test - @DisplayName("이름으로 예약을 조회한다.") - void getReservationsByName() { + @DisplayName("멤버 ID로 예약을 조회한다.") + void getReservationsByMemberId() { + Member member = createMember("테스터"); ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); Theme theme = createTheme("테마"); reservationService.createReservation( - new ReservationCreationRequest("테스터", date.getId(), time.getId(), theme.getId())); + new ReservationCreationRequest(member.getId(), date.getId(), time.getId(), theme.getId())); - List responses = reservationService.getReservationsByName("테스터"); + List responses = reservationService.getReservationsByMemberId(member.getId()); assertThat(responses).hasSize(1); assertThat(responses.get(0).name()).isEqualTo("테스터"); @@ -103,15 +113,17 @@ void getReservationsByName() { @Test @DisplayName("모든 예약을 조회한다.") void getAllReservations() { + Member member1 = createMember("테스터1"); + Member member2 = createMember("테스터2"); ReservationDate date1 = createDate(LocalDate.now().plusDays(1)); ReservationDate date2 = createDate(LocalDate.now().plusDays(2)); ReservationTime time1 = createTime(LocalTime.of(10, 0)); ReservationTime time2 = createTime(LocalTime.of(11, 0)); Theme theme = createTheme("테마"); reservationService.createReservation( - new ReservationCreationRequest("테스터1", date1.getId(), time1.getId(), theme.getId())); + new ReservationCreationRequest(member1.getId(), date1.getId(), time1.getId(), theme.getId())); reservationService.createReservation( - new ReservationCreationRequest("테스터2", date2.getId(), time2.getId(), theme.getId())); + new ReservationCreationRequest(member2.getId(), date2.getId(), time2.getId(), theme.getId())); List responses = reservationService.getAllReservations(); @@ -121,11 +133,12 @@ void getAllReservations() { @Test @DisplayName("예약을 취소한다.") void cancelReservation() { + Member member = createMember("테스터"); ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); Theme theme = createTheme("테마"); ReservationCreationResponse response = reservationService.createReservation( - new ReservationCreationRequest("테스터", date.getId(), time.getId(), theme.getId())); + new ReservationCreationRequest(member.getId(), date.getId(), time.getId(), theme.getId())); reservationService.cancelReservation(response.id()); @@ -135,11 +148,12 @@ void cancelReservation() { @Test @DisplayName("관리자가 예약을 삭제한다.") void deleteReservation() { + Member member = createMember("테스터"); ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); Theme theme = createTheme("테마"); ReservationCreationResponse response = reservationService.createReservation( - new ReservationCreationRequest("테스터", date.getId(), time.getId(), theme.getId())); + new ReservationCreationRequest(member.getId(), date.getId(), time.getId(), theme.getId())); reservationService.deleteReservation(response.id()); @@ -149,31 +163,33 @@ void deleteReservation() { @Test @DisplayName("당일 예약 취소 시 예외가 발생한다.") void cancelTodayReservation() { + Member member = createMember("당일예약테스터"); ReservationDate date = createDate(LocalDate.now()); ReservationTime time = createTime(LocalTime.of(23, 59)); Theme theme = createTheme("테마"); Reservation reservation = reservationRepository.save( - Reservation.createWithoutId("당일예약테스터", date, time, theme)); + Reservation.createWithoutId(member, date, time, theme)); assertThatThrownBy(() -> reservationService.cancelReservation(reservation.getId())) - .isInstanceOf(RoomescapeException.class) - .hasMessageContaining(ReservationDateErrorCode.TODAY_NOT_MODIFIED.getMessage()); + .isInstanceOf(RoomescapeException.class) + .hasMessageContaining(ReservationDateErrorCode.TODAY_NOT_MODIFIED.getMessage()); } @Test @DisplayName("예약을 수정한다.") void updateReservation() { + Member member = createMember("테스터"); ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); Theme theme = createTheme("테마"); ReservationCreationResponse created = reservationService.createReservation( - new ReservationCreationRequest("테스터", date.getId(), time.getId(), theme.getId())); + new ReservationCreationRequest(member.getId(), date.getId(), time.getId(), theme.getId())); ReservationDate newDate = createDate(LocalDate.now().plusDays(2)); ReservationTime newTime = createTime(LocalTime.of(14, 0)); ReservationResponse updated = reservationService.updateReservation( - created.id(), new ReservationUpdateRequest(newDate.getId(), newTime.getId())); + created.id(), new ReservationUpdateRequest(newDate.getId(), newTime.getId())); assertThat(updated.date()).isEqualTo(newDate.getPlayDay()); assertThat(updated.time().id()).isEqualTo(newTime.getId()); @@ -182,52 +198,60 @@ void updateReservation() { @Test @DisplayName("과거 시간으로 예약 수정 시 예외가 발생한다.") void updateReservationWithPastTime() { + Member member = createMember("테스터"); ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); Theme theme = createTheme("테마"); ReservationCreationResponse created = reservationService.createReservation( - new ReservationCreationRequest("테스터", date.getId(), time.getId(), theme.getId())); + new ReservationCreationRequest(member.getId(), date.getId(), time.getId(), theme.getId())); ReservationDate pastDate = createDate(LocalDate.now().minusDays(1)); assertThatThrownBy(() -> reservationService.updateReservation( - created.id(), new ReservationUpdateRequest(pastDate.getId(), time.getId()))) - .isInstanceOf(RoomescapeException.class) - .hasMessageContaining(ReservationDateErrorCode.PAST_DATE_NOT_ALLOWED.getMessage()); + created.id(), new ReservationUpdateRequest(pastDate.getId(), time.getId()))) + .isInstanceOf(RoomescapeException.class) + .hasMessageContaining(ReservationDateErrorCode.PAST_DATE_NOT_ALLOWED.getMessage()); } @Test @DisplayName("이미 존재하는 시간으로 예약 수정 시 예외가 발생한다.") void updateReservationToDuplicatedTime() { + Member member1 = createMember("내예약"); + Member member2 = createMember("다른사람예약"); ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = createTime(LocalTime.of(10, 0)); ReservationTime anotherTime = createTime(LocalTime.of(14, 0)); Theme theme = createTheme("테마"); ReservationCreationResponse myReservation = reservationService.createReservation( - new ReservationCreationRequest("내예약", date.getId(), time.getId(), theme.getId())); + new ReservationCreationRequest(member1.getId(), date.getId(), time.getId(), theme.getId())); reservationService.createReservation( - new ReservationCreationRequest("다른사람예약", date.getId(), anotherTime.getId(), theme.getId())); + new ReservationCreationRequest(member2.getId(), date.getId(), anotherTime.getId(), theme.getId())); assertThatThrownBy(() -> reservationService.updateReservation( - myReservation.id(), new ReservationUpdateRequest(date.getId(), anotherTime.getId()))) - .isInstanceOf(RoomescapeException.class) - .hasMessageContaining(ReservationErrorCode.RESERVATION_DUPLICATED.getMessage()); + myReservation.id(), new ReservationUpdateRequest(date.getId(), anotherTime.getId()))) + .isInstanceOf(RoomescapeException.class) + .hasMessageContaining(ReservationErrorCode.RESERVATION_DUPLICATED.getMessage()); } @Test @DisplayName("당일 예약 수정 시 예외가 발생한다.") void updateTodayReservation() { + Member member = createMember("당일예약테스터"); ReservationDate date = createDate(LocalDate.now()); ReservationTime time = createTime(LocalTime.of(23, 59)); ReservationTime newTime = createTime(LocalTime.of(14, 0)); Theme theme = createTheme("테마"); Reservation reservation = reservationRepository.save( - Reservation.createWithoutId("당일예약테스터", date, time, theme)); + Reservation.createWithoutId(member, date, time, theme)); assertThatThrownBy(() -> reservationService.updateReservation( - reservation.getId(), new ReservationUpdateRequest(date.getId(), newTime.getId()))) - .isInstanceOf(RoomescapeException.class) - .hasMessageContaining(ReservationDateErrorCode.TODAY_NOT_MODIFIED.getMessage()); + reservation.getId(), new ReservationUpdateRequest(date.getId(), newTime.getId()))) + .isInstanceOf(RoomescapeException.class) + .hasMessageContaining(ReservationDateErrorCode.TODAY_NOT_MODIFIED.getMessage()); + } + + private Member createMember(String name) { + return memberRepository.save(Member.createWithoutId(name)); } private ReservationDate createDate(LocalDate playDay) { diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 30ca648d9c..4b3ab20bb0 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -7,28 +7,27 @@ import java.time.LocalDate; import java.time.LocalTime; import org.junit.jupiter.api.Test; +import roomescape.domain.member.Member; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; +import roomescape.support.exception.MemberErrorCode; import roomescape.support.exception.RoomescapeException; class ReservationTest { @Test void id가_없는_예약을_생성한다() { - // given - String name = "보예"; + Member member = Member.of(1L, "보예"); ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); - // when - Reservation reservation = Reservation.createWithoutId(name, date, time, theme); + Reservation reservation = Reservation.createWithoutId(member, date, time, theme); - // then assertSoftly(softly -> { softly.assertThat(reservation.getId()).isNull(); - softly.assertThat(reservation.getName()).isEqualTo(name); + softly.assertThat(reservation.getMember()).isEqualTo(member); softly.assertThat(reservation.getDate()).isEqualTo(date); softly.assertThat(reservation.getTime()).isEqualTo(time); softly.assertThat(reservation.getTheme()).isEqualTo(theme); @@ -38,30 +37,17 @@ class ReservationTest { @Test void id를_부여한_예약을_생성한다() { - // given + Member member = Member.of(1L, "보예"); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); - Reservation reservation = Reservation.createWithoutId( - "보예", - date, - time, - theme - ); + Reservation reservation = Reservation.createWithoutId(member, date, time, theme); - // when - Reservation reservationWithId = Reservation.of( - 1L, - reservation.getName(), - reservation.getDate(), - reservation.getTime(), - reservation.getTheme() - ); + Reservation reservationWithId = Reservation.of(1L, member, date, time, theme); - // then assertSoftly(softly -> { assertThat(reservationWithId.getId()).isEqualTo(1L); - assertThat(reservationWithId.getName()).isEqualTo("보예"); + assertThat(reservationWithId.getMember()).isEqualTo(member); assertThat(reservationWithId.getDate()).isEqualTo(date); assertThat(reservationWithId.getTime()).isEqualTo(time); assertThat(reservationWithId.getTheme()).isEqualTo(theme); @@ -71,20 +57,17 @@ class ReservationTest { @Test void DB에서_조회한_예약을_생성한다() { - // given long id = 1L; - String name = "보예"; + Member member = Member.of(2L, "보예"); ReservationDate date = ReservationDate.of(2L, LocalDate.of(2023, 8, 5)); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); - // when - Reservation reservation = Reservation.of(id, name, date, time, theme); + Reservation reservation = Reservation.of(id, member, date, time, theme); - // then assertSoftly(softly -> { assertThat(reservation.getId()).isEqualTo(id); - assertThat(reservation.getName()).isEqualTo(name); + assertThat(reservation.getMember()).isEqualTo(member); assertThat(reservation.getDate()).isEqualTo(date); assertThat(reservation.getTime()).isEqualTo(time); assertThat(reservation.getTheme()).isEqualTo(theme); @@ -93,89 +76,65 @@ class ReservationTest { } @Test - void 이름이_null이면_예외가_발생한다() { - // given - String name = null; - ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); - ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); - Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); - - // when & then - assertThatThrownBy(() -> Reservation.createWithoutId(name, date, time, theme)) - .isInstanceOf(RoomescapeException.class) - .hasMessage("예약자 성명 데이터가 유효하지 않습니다."); - } - - @Test - void 이름이_공백이면_예외가_발생한다() { - // given - String name = " "; + void 멤버가_null이면_예외가_발생한다() { + Member member = null; ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); - // when & then - assertThatThrownBy(() -> Reservation.createWithoutId(name, date, time, theme)) + assertThatThrownBy(() -> Reservation.createWithoutId(member, date, time, theme)) .isInstanceOf(RoomescapeException.class) - .hasMessage("예약자 성명 데이터가 유효하지 않습니다."); + .hasMessage(MemberErrorCode.INVALID_MEMBER.getMessage()); } @Test void 날짜가_null이면_예외가_발생한다() { - // given - String name = "보예"; + Member member = Member.of(1L, "보예"); ReservationDate date = null; ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); - // when & hen - assertThatThrownBy(() -> Reservation.createWithoutId(name, date, time, theme)) + assertThatThrownBy(() -> Reservation.createWithoutId(member, date, time, theme)) .isInstanceOf(RoomescapeException.class) .hasMessage("예약 날짜 식별자 혹은 데이터가 누락되었습니다."); } @Test void 예약_시간이_null이면_예외가_발생한다() { - // given - String name = "보예"; + Member member = Member.of(1L, "보예"); ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); ReservationTime time = null; Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); - // when & then - assertThatThrownBy(() -> Reservation.createWithoutId(name, date, time, theme)) + assertThatThrownBy(() -> Reservation.createWithoutId(member, date, time, theme)) .isInstanceOf(RoomescapeException.class) .hasMessage("예약 시간 식별자 정보가 누락되었습니다."); } @Test void 테마가_null이면_예외가_발생한다() { - // given - String name = "보예"; + Member member = Member.of(1L, "보예"); ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); Theme theme = null; - // when & then - assertThatThrownBy(() -> Reservation.createWithoutId(name, date, time, theme)) + assertThatThrownBy(() -> Reservation.createWithoutId(member, date, time, theme)) .isInstanceOf(RoomescapeException.class) .hasMessage("테마 엔티티 식별자 정보가 누락되었습니다."); } @Test void 날짜_시간을_업데이트_할_수_있다() { - // given + Member member = Member.of(1L, "이산"); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); - ReservationTime updateTime = ReservationTime.createWithoutId(LocalTime.of(10, 00)); + ReservationTime updateTime = ReservationTime.createWithoutId(LocalTime.of(10, 0)); ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2026, 8, 5)); ReservationDate updateDate = ReservationDate.createWithoutId(LocalDate.of(2026, 8, 15)); Theme theme = Theme.of(1L, "공포", "무서운 테마", "theme-url"); - Reservation reservation = Reservation.createWithoutId("이산", date, time, theme); + Reservation reservation = Reservation.createWithoutId(member, date, time, theme); - // when reservation.update(updateDate, updateTime); - // then assertThat(reservation.getDate()).isEqualTo(updateDate); assertThat(reservation.getTime()).isEqualTo(updateTime); } diff --git a/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java index 3814f472e1..837584ca16 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationUpdatingIntegrationTest.java @@ -14,6 +14,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.dto.ReservationUpdateRequest; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; @@ -37,6 +39,9 @@ class ReservationUpdatingIntegrationTest { @MockitoSpyBean private WaitingReservationRepository waitingReservationRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired private ReservationDateRepository reservationDateRepository; @@ -49,18 +54,15 @@ class ReservationUpdatingIntegrationTest { private Slot originSlot; private Slot updateSlot; private Reservation originReservation; + private Member tester; @BeforeEach void setUp() { + tester = memberRepository.save(Member.createWithoutId("테스터")); originSlot = insertSlot(LocalDate.now().plusDays(2), LocalTime.of(10, 0), "공포"); originReservation = reservationRepository.save( - Reservation.createWithoutId( - "테스터", - originSlot.date(), - originSlot.time(), - originSlot.theme() - ) + Reservation.createWithoutId(tester, originSlot.date(), originSlot.time(), originSlot.theme()) ); updateSlot = insertSlot(LocalDate.now().plusDays(3), LocalTime.of(10, 0), "공포"); @@ -68,51 +70,63 @@ void setUp() { @Test void 사용자가_본인의_예약을_수정하면_같은_슬롯의_1순위_대기가_예약으로_변경된다() { - ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); + Member isan = memberRepository.save(Member.createWithoutId("이산")); + Member gorae = memberRepository.save(Member.createWithoutId("고래")); + + ReservationUpdateRequest updateRequest = new ReservationUpdateRequest( + updateSlot.date().getId(), updateSlot.time().getId()); WaitingReservation firstWaiting = waitingReservationRepository.save( - waiting("이산", originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + waiting(isan, originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) ); WaitingReservation secondWaiting = waitingReservationRepository.save( - waiting("고래", originSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) + waiting(gorae, originSlot, LocalDateTime.of(2026, 5, 7, 10, 0)) ); reservationService.updateReservation(originReservation.getId(), updateRequest); - assertThat(reservationRepository.findByName("테스터")).hasSize(1); - assertThat(reservationRepository.findByName("이산")).hasSize(1); - assertThat(reservationRepository.existsByDateIdAndTimeIdAndThemeId(originSlot.date.getId(), originSlot.time.getId(), originSlot.theme.getId())).isTrue(); - assertThat(reservationRepository.existsByDateIdAndTimeIdAndThemeId(updateSlot.date.getId(), updateSlot.time.getId(), originSlot.theme.getId())).isTrue(); + assertThat(reservationRepository.findByMemberId(tester.getId())).hasSize(1); + assertThat(reservationRepository.findByMemberId(isan.getId())).hasSize(1); + assertThat(reservationRepository.existsByDateIdAndTimeIdAndThemeId( + originSlot.date().getId(), originSlot.time().getId(), originSlot.theme().getId())).isTrue(); + assertThat(reservationRepository.existsByDateIdAndTimeIdAndThemeId( + updateSlot.date().getId(), updateSlot.time().getId(), originSlot.theme().getId())).isTrue(); assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isEmpty(); assertThat(waitingReservationRepository.findById(secondWaiting.getId())).isPresent(); } @Test void 예약_수정_중_1순위_예약_대기_추가가_실패하면_전체가_롤백된다() { - ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); + Member isan = memberRepository.save(Member.createWithoutId("이산")); + + ReservationUpdateRequest updateRequest = new ReservationUpdateRequest( + updateSlot.date().getId(), updateSlot.time().getId()); WaitingReservation firstWaiting = waitingReservationRepository.save( - waiting("이산", originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + waiting(isan, originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) ); doThrow(new RuntimeException()) .when(reservationRepository) .save(any(Reservation.class)); - assertThatThrownBy(() -> reservationService.updateReservation(originReservation.getId(), updateRequest)) + assertThatThrownBy(() -> reservationService.updateReservation(originReservation.getId(), updateRequest)) .isInstanceOf(RuntimeException.class); assertThat(reservationRepository.findById(originReservation.getId())).isPresent(); assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); - assertThat(reservationRepository.findByName("이산")).isEmpty(); + assertThat(reservationRepository.findByMemberId(isan.getId())).isEmpty(); } @Test void 예약_수정_중_1순위_예약_대기_삭제가_실패하면_전체가_롤백된다() { - ReservationUpdateRequest updateRequest = new ReservationUpdateRequest(updateSlot.date.getId(), updateSlot.time.getId()); + Member isan = memberRepository.save(Member.createWithoutId("이산")); + + ReservationUpdateRequest updateRequest = new ReservationUpdateRequest( + updateSlot.date().getId(), updateSlot.time().getId()); WaitingReservation firstWaiting = waitingReservationRepository.save( - waiting("이산", originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) + waiting(isan, originSlot, LocalDateTime.of(2026, 5, 6, 10, 0)) ); doThrow(new RuntimeException()).when(waitingReservationRepository).deleteById(firstWaiting.getId()); @@ -122,11 +136,11 @@ void setUp() { assertThat(reservationRepository.findById(originReservation.getId())).isPresent(); assertThat(waitingReservationRepository.findById(firstWaiting.getId())).isPresent(); - assertThat(reservationRepository.findByName("이산")).isEmpty(); + assertThat(reservationRepository.findByMemberId(isan.getId())).isEmpty(); } - private WaitingReservation waiting(String name, Slot slot, LocalDateTime createdAt) { - return WaitingReservation.createWithoutId(name, slot.date(), slot.time(), slot.theme(), createdAt); + private WaitingReservation waiting(Member member, Slot slot, LocalDateTime createdAt) { + return WaitingReservation.createWithoutId(member, slot.date(), slot.time(), slot.theme(), createdAt); } private Slot insertSlot(LocalDate playDay, LocalTime startAt, String themeName) { @@ -136,11 +150,6 @@ private Slot insertSlot(LocalDate playDay, LocalTime startAt, String themeName) return new Slot(date, time, theme); } - private record Slot( - ReservationDate date, - ReservationTime time, - Theme theme - ) { - + private record Slot(ReservationDate date, ReservationTime time, Theme theme) { } } diff --git a/src/test/java/roomescape/domain/reservation/dto/ReservationCreationRequestTest.java b/src/test/java/roomescape/domain/reservation/dto/ReservationCreationRequestTest.java index 67874fd2f6..a35a08fdab 100644 --- a/src/test/java/roomescape/domain/reservation/dto/ReservationCreationRequestTest.java +++ b/src/test/java/roomescape/domain/reservation/dto/ReservationCreationRequestTest.java @@ -21,59 +21,23 @@ static void setUp() { } @Test - void 이름이_null이면_예외가_발생한다() { - // given - ReservationCreationRequest request = new ReservationCreationRequest( - null, - 1L, - 1L, - 1L - ); - - // when - Set> violations = validator.validate(request); - - // then - assertThat(violations).hasSize(1); - assertThat(violations) - .extracting(ConstraintViolation::getMessage) - .contains("예약자명은 필수입니다"); - } + void memberId가_null이면_예외가_발생한다() { + ReservationCreationRequest request = new ReservationCreationRequest(null, 1L, 1L, 1L); - @Test - void 이름이_공백이면_예외가_발생한다() { - // given - ReservationCreationRequest request = new ReservationCreationRequest( - " ", - 1L, - 1L, - 1L - ); - - // when Set> violations = validator.validate(request); - // then assertThat(violations).hasSize(1); assertThat(violations) .extracting(ConstraintViolation::getMessage) - .contains("예약자명은 필수입니다"); + .contains("멤버 ID는 필수입니다"); } @Test void 날짜가_null이면_예외가_발생한다() { - // given - ReservationCreationRequest request = new ReservationCreationRequest( - "보예", - null, - 1L, - 1L - ); - - // when + ReservationCreationRequest request = new ReservationCreationRequest(1L, null, 1L, 1L); + Set> violations = validator.validate(request); - // then assertThat(violations).hasSize(1); assertThat(violations) .extracting(ConstraintViolation::getMessage) @@ -82,18 +46,10 @@ static void setUp() { @Test void 시간_id가_null이면_예외가_발생한다() { - // given - ReservationCreationRequest request = new ReservationCreationRequest( - "보예", - 1L, - null, - 1L - ); - - // when + ReservationCreationRequest request = new ReservationCreationRequest(1L, 1L, null, 1L); + Set> violations = validator.validate(request); - // then assertThat(violations).hasSize(1); assertThat(violations) .extracting(ConstraintViolation::getMessage) @@ -102,18 +58,10 @@ static void setUp() { @Test void 테마_id가_null이면_예외가_발생한다() { - // given - ReservationCreationRequest request = new ReservationCreationRequest( - "보예", - 1L, - 1L, - null - ); - - // when + ReservationCreationRequest request = new ReservationCreationRequest(1L, 1L, 1L, null); + Set> violations = validator.validate(request); - // then assertThat(violations).hasSize(1); assertThat(violations) .extracting(ConstraintViolation::getMessage) diff --git a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java index 0f77def667..a5214ea5c1 100644 --- a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java +++ b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java @@ -10,6 +10,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.dto.ReservationDateCreationRequest; @@ -42,6 +44,9 @@ class ReservationDateServiceTest { @Autowired private ThemeRepository themeRepository; + @Autowired + private MemberRepository memberRepository; + @Test @DisplayName("예약 날짜를 생성한다.") void createReservationDate() { @@ -80,7 +85,8 @@ void deleteInUseDate() { ReservationDate date = createDate(LocalDate.now().plusDays(1)); ReservationTime time = reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(10, 0))); Theme theme = themeRepository.save(Theme.createWithoutId("테스트테마", "설명", "url")); - reservationRepository.save(Reservation.createWithoutId("테스터", date, time, theme)); + Member tester = memberRepository.save(Member.createWithoutId("테스터")); + reservationRepository.save(Reservation.createWithoutId(tester, date, time, theme)); assertThatThrownBy(() -> reservationDateService.deleteReservationDate(date.getId())) .isInstanceOf(RoomescapeException.class); diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java index de4944b351..b1f3a3ceb6 100644 --- a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java @@ -10,6 +10,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.ReservationDate; @@ -40,6 +42,9 @@ class ReservationTimeServiceTest { @Autowired private ThemeRepository themeRepository; + @Autowired + private MemberRepository memberRepository; + @Test void 예약_시간을_생성한다() { TimeCreationRequest request = new TimeCreationRequest(LocalTime.of(10, 0)); @@ -64,7 +69,8 @@ class ReservationTimeServiceTest { ReservationTime time2 = createTime(LocalTime.of(11, 0)); ReservationDate date = reservationDateRepository.save(ReservationDate.createWithoutId(LocalDate.now().plusDays(1))); Theme theme = themeRepository.save(Theme.createWithoutId("테스트테마", "설명", "url")); - reservationRepository.save(Reservation.createWithoutId("테스터", date, time1, theme)); + Member tester = memberRepository.save(Member.createWithoutId("테스터")); + reservationRepository.save(Reservation.createWithoutId(tester, date, time1, theme)); List responses = reservationTimeService.getReservationTimeAvailability(theme.getId(), date.getId()); @@ -79,7 +85,8 @@ class ReservationTimeServiceTest { ReservationTime time = createTime(LocalTime.of(10, 0)); ReservationDate date = reservationDateRepository.save(ReservationDate.createWithoutId(LocalDate.now().plusDays(1))); Theme theme = themeRepository.save(Theme.createWithoutId("테스트테마", "설명", "url")); - reservationRepository.save(Reservation.createWithoutId("테스터", date, time, theme)); + Member tester = memberRepository.save(Member.createWithoutId("테스터")); + reservationRepository.save(Reservation.createWithoutId(tester, date, time, theme)); assertThatThrownBy(() -> reservationTimeService.deleteReservationTime(time.getId())) .isInstanceOf(RoomescapeException.class); diff --git a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java index 437d224597..1515c51c1e 100644 --- a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java @@ -11,6 +11,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.ReservationDate; @@ -41,6 +43,9 @@ class ThemeServiceTest { @Autowired private ReservationTimeRepository reservationTimeRepository; + @Autowired + private MemberRepository memberRepository; + @Test @DisplayName("테마를 생성한다.") void createTheme() { @@ -57,7 +62,8 @@ void deleteInUseTheme() { Theme theme = createTheme("테마"); ReservationDate date = reservationDateRepository.save(ReservationDate.createWithoutId(LocalDate.now().plusDays(1))); ReservationTime time = reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(10, 0))); - reservationRepository.save(Reservation.createWithoutId("테스터", date, time, theme)); + Member tester = memberRepository.save(Member.createWithoutId("테스터")); + reservationRepository.save(Reservation.createWithoutId(tester, date, time, theme)); assertThatThrownBy(() -> themeService.deleteTheme(theme.getId())) .isInstanceOf(RoomescapeException.class); diff --git a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationControllerTest.java b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationControllerTest.java index e048f79db9..0adb4d261f 100644 --- a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationControllerTest.java +++ b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationControllerTest.java @@ -15,6 +15,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.jdbc.Sql; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.ReservationDate; @@ -31,6 +33,9 @@ class WaitingReservationControllerTest { @LocalServerPort private int port; + @Autowired + private MemberRepository memberRepository; + @Autowired private ReservationDateRepository reservationDateRepository; @@ -52,6 +57,8 @@ class WaitingReservationControllerTest { private Long theme1Id; private Long theme4Id; private Long firstWaitingId; + private Long goraeMemberId; + private Long existingWaiterMemberId; @BeforeEach void setUp() { @@ -75,111 +82,119 @@ void setUp() { theme1Id = theme1.getId(); theme4Id = theme4.getId(); - reservationRepository.save(Reservation.createWithoutId("기존예약자", date1, time1, theme1)); - reservationRepository.save(Reservation.createWithoutId("기존예약자", date2, time2, theme2)); - reservationRepository.save(Reservation.createWithoutId("기존예약자", date3, time3, theme3)); - reservationRepository.save(Reservation.createWithoutId("기존예약자", date1, time1, theme4)); + Member existingReserver = memberRepository.save(Member.createWithoutId("기존예약자")); + Member existingWaiter = memberRepository.save(Member.createWithoutId("기존대기자")); + Member gorae = memberRepository.save(Member.createWithoutId("고래")); + Member namu = memberRepository.save(Member.createWithoutId("나무")); + Member isan = memberRepository.save(Member.createWithoutId("이산")); + goraeMemberId = gorae.getId(); + existingWaiterMemberId = existingWaiter.getId(); + + reservationRepository.save(Reservation.createWithoutId(existingReserver, date1, time1, theme1)); + reservationRepository.save(Reservation.createWithoutId(existingReserver, date2, time2, theme2)); + reservationRepository.save(Reservation.createWithoutId(existingReserver, date3, time3, theme3)); + reservationRepository.save(Reservation.createWithoutId(existingReserver, date1, time1, theme4)); WaitingReservation firstWaiting = waitingReservationRepository.save( - WaitingReservation.createWithoutId("기존대기자", date1, time1, theme1, LocalDateTime.now().minusHours(2))); + WaitingReservation.createWithoutId(existingWaiter, date1, time1, theme1, LocalDateTime.now().minusHours(2))); firstWaitingId = firstWaiting.getId(); - waitingReservationRepository.save(WaitingReservation.createWithoutId("고래", date1, time1, theme1, LocalDateTime.now().minusHours(1))); - waitingReservationRepository.save(WaitingReservation.createWithoutId("나무", date2, time2, theme2, LocalDateTime.now().minusHours(3))); - waitingReservationRepository.save(WaitingReservation.createWithoutId("이산", date2, time2, theme2, LocalDateTime.now().minusHours(2))); - waitingReservationRepository.save(WaitingReservation.createWithoutId("고래", date2, time2, theme2, LocalDateTime.now().minusHours(1))); - waitingReservationRepository.save(WaitingReservation.createWithoutId("고래", date3, time3, theme3, LocalDateTime.now())); + waitingReservationRepository.save(WaitingReservation.createWithoutId(gorae, date1, time1, theme1, LocalDateTime.now().minusHours(1))); + waitingReservationRepository.save(WaitingReservation.createWithoutId(namu, date2, time2, theme2, LocalDateTime.now().minusHours(3))); + waitingReservationRepository.save(WaitingReservation.createWithoutId(isan, date2, time2, theme2, LocalDateTime.now().minusHours(2))); + waitingReservationRepository.save(WaitingReservation.createWithoutId(gorae, date2, time2, theme2, LocalDateTime.now().minusHours(1))); + waitingReservationRepository.save(WaitingReservation.createWithoutId(gorae, date3, time3, theme3, LocalDateTime.now())); } @Test void 사용자는_예약_대기를_신청한다() { Map params = new HashMap<>(); - params.put("name", "고래"); + params.put("memberId", goraeMemberId); params.put("dateId", date1Id); params.put("timeId", time1Id); params.put("themeId", theme4Id); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when() - .post("/waiting-reservations") - .then().log().all() - .statusCode(201) - .body("name", is("고래")) - .body("theme.name", is("코미디테마")); + .contentType(ContentType.JSON) + .body(params) + .when() + .post("/waiting-reservations") + .then().log().all() + .statusCode(201) + .body("name", is("고래")) + .body("theme.name", is("코미디테마")); } @Test void 중복_예약_대기_신청을_하면_409를_반환한다() { Map params = new HashMap<>(); - params.put("name", "기존대기자"); + params.put("memberId", existingWaiterMemberId); params.put("dateId", date1Id); params.put("timeId", time1Id); params.put("themeId", theme1Id); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when() - .post("/waiting-reservations") - .then().log().all() - .statusCode(409); + .contentType(ContentType.JSON) + .body(params) + .when() + .post("/waiting-reservations") + .then().log().all() + .statusCode(409); } @Test void 예약_가능한_시간에_대기_신청하면_409을_반환한다() { Map params = new HashMap<>(); - params.put("name", "고래"); + params.put("memberId", goraeMemberId); params.put("dateId", date1Id); params.put("timeId", time2Id); params.put("themeId", theme1Id); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when() - .post("/waiting-reservations") - .then().log().all() - .statusCode(409); + .contentType(ContentType.JSON) + .body(params) + .when() + .post("/waiting-reservations") + .then().log().all() + .statusCode(409); } @Test void 존재하지_않는_슬롯에_대기_신청을_하면_404을_반환한다() { Map params = new HashMap<>(); - params.put("name", "고래"); + params.put("memberId", goraeMemberId); params.put("dateId", 999); params.put("timeId", 999); params.put("themeId", 999); RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when() - .post("/waiting-reservations") - .then().log().all() - .statusCode(404); + .contentType(ContentType.JSON) + .body(params) + .when() + .post("/waiting-reservations") + .then().log().all() + .statusCode(404); } @Test void 사용자는_예약_대기를_취소한다() { RestAssured.given().log().all() - .contentType(ContentType.JSON) - .when() - .delete("/waiting-reservations/" + firstWaitingId) - .then().log().all() - .statusCode(204); + .contentType(ContentType.JSON) + .when() + .delete("/waiting-reservations/" + firstWaitingId) + .then().log().all() + .statusCode(204); } @Test void 예약_대기_목록과_순번을_조회한다() { RestAssured.given().log().all() - .param("name", "고래") - .when().get("/waiting-reservations") - .then().log().all() - .statusCode(200) - .body("size()", is(3)) - .body("find {it.theme.name == '테스트테마'}.rank", is(2)) - .body("find {it.theme.name == '공포테마'}.rank", is(3)) - .body("find {it.theme.name == '스릴러테마'}.rank", is(1)); + .param("memberId", goraeMemberId) + .when().get("/waiting-reservations") + .then().log().all() + .statusCode(200) + .body("size()", is(3)) + .body("find {it.theme.name == '테스트테마'}.rank", is(2)) + .body("find {it.theme.name == '공포테마'}.rank", is(3)) + .body("find {it.theme.name == '스릴러테마'}.rank", is(1)); } } diff --git a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationControllerValidationTest.java b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationControllerValidationTest.java index e9a3e733ec..1ff6d00ead 100644 --- a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationControllerValidationTest.java +++ b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationControllerValidationTest.java @@ -25,7 +25,7 @@ class WaitingReservationControllerValidationTest { private AdminRequestValidator adminRequestValidator; @Test - void 예약자명이_없으면_400을_반환한다() throws Exception { + void memberId가_없으면_400을_반환한다() throws Exception { String requestBody = """ { "dateId": 1, @@ -40,28 +40,11 @@ class WaitingReservationControllerValidationTest { .andExpect(status().isBadRequest()); } - @Test - void 예약자명이_공백이면_400을_반환한다() throws Exception { - String requestBody = """ - { - "name": " ", - "dateId": 1, - "timeId": 1, - "themeId": 1 - } - """; - - mockMvc.perform(post("/waiting-reservations") - .contentType(MediaType.APPLICATION_JSON) - .content(requestBody)) - .andExpect(status().isBadRequest()); - } - @Test void dateId가_없으면_400을_반환한다() throws Exception { String requestBody = """ { - "name": "고래", + "memberId": 1, "timeId": 1, "themeId": 1 } @@ -77,7 +60,7 @@ class WaitingReservationControllerValidationTest { void timeId가_없으면_400을_반환한다() throws Exception { String requestBody = """ { - "name": "고래", + "memberId": 1, "dateId": 1, "themeId": 1 } @@ -93,7 +76,7 @@ class WaitingReservationControllerValidationTest { void themeId가_없으면_400을_반환한다() throws Exception { String requestBody = """ { - "name": "고래", + "memberId": 1, "dateId": 1, "timeId": 1 } @@ -106,7 +89,7 @@ class WaitingReservationControllerValidationTest { } @Test - void name_파라미터가_없으면_400을_반환한다() throws Exception { + void memberId_파라미터가_없으면_400을_반환한다() throws Exception { mockMvc.perform(get("/waiting-reservations")) .andExpect(status().isBadRequest()); } diff --git a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java index 319d0deb39..7a0308d9b5 100644 --- a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java +++ b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationServiceTest.java @@ -14,6 +14,8 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberService; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateService; @@ -31,6 +33,7 @@ class WaitingReservationServiceTest { private ReservationRepository reservationRepository; private WaitingReservationRepository waitingReservationRepository; + private MemberService memberService; private ReservationDateService reservationDateService; private ReservationTimeService reservationTimeService; private ThemeService themeService; @@ -40,12 +43,14 @@ class WaitingReservationServiceTest { void setUp() { reservationRepository = mock(ReservationRepository.class); waitingReservationRepository = mock(WaitingReservationRepository.class); + memberService = mock(MemberService.class); reservationDateService = mock(ReservationDateService.class); reservationTimeService = mock(ReservationTimeService.class); themeService = mock(ThemeService.class); waitingReservationService = new WaitingReservationService( waitingReservationRepository, reservationRepository, + memberService, reservationDateService, reservationTimeService, themeService @@ -54,19 +59,16 @@ void setUp() { @Test void 이미_다른_사용자에_의해_예약된_슬롯에_대기를_신청할_수_있다() { + Member member = Member.of(1L, "고래"); ReservationDate date = ReservationDate.of(1L, LocalDate.now().plusDays(5)); ReservationTime time = ReservationTime.of(2L, LocalTime.of(10, 0)); Theme theme = Theme.of(3L, "공포", "테마 내용", "/themes/scary"); - WaitingReservationCreationRequest request = new WaitingReservationCreationRequest("고래", 1L, 2L, 3L); + WaitingReservationCreationRequest request = new WaitingReservationCreationRequest(1L, 1L, 2L, 3L); WaitingReservation savedWaiting = WaitingReservation.of( - 10L, - "고래", - date, - time, - theme, - LocalDateTime.of(2026, 5, 5, 14, 0) + 10L, member, date, time, theme, LocalDateTime.of(2026, 5, 5, 14, 0) ); + when(memberService.findById(1L)).thenReturn(member); when(reservationDateService.findById(1L)).thenReturn(date); when(reservationTimeService.findById(2L)).thenReturn(time); when(themeService.findById(3L)).thenReturn(theme); @@ -84,11 +86,13 @@ void setUp() { @Test void 비어있는_슬롯에_대기를_신청하면_예외가_발생한다() { + Member member = Member.of(1L, "고래"); ReservationDate date = ReservationDate.of(1L, LocalDate.now().plusDays(5)); ReservationTime time = ReservationTime.of(2L, LocalTime.of(10, 0)); Theme theme = Theme.of(3L, "공포", "테마 내용", "/themes/scary"); - WaitingReservationCreationRequest request = new WaitingReservationCreationRequest("고래", 1L, 2L, 3L); + WaitingReservationCreationRequest request = new WaitingReservationCreationRequest(1L, 1L, 2L, 3L); + when(memberService.findById(1L)).thenReturn(member); when(reservationDateService.findById(1L)).thenReturn(date); when(reservationTimeService.findById(2L)).thenReturn(time); when(themeService.findById(3L)).thenReturn(theme); @@ -101,16 +105,18 @@ void setUp() { @Test void 같은_사용자가_같은_슬롯에_중복_대기할_수_없다() { + Member member = Member.of(1L, "고래"); ReservationDate date = ReservationDate.of(1L, LocalDate.now().plusDays(5)); ReservationTime time = ReservationTime.of(2L, LocalTime.of(10, 0)); Theme theme = Theme.of(3L, "공포", "테마 내용", "/themes/scary"); - WaitingReservationCreationRequest request = new WaitingReservationCreationRequest("고래", 1L, 2L, 3L); + WaitingReservationCreationRequest request = new WaitingReservationCreationRequest(1L, 1L, 2L, 3L); + when(memberService.findById(1L)).thenReturn(member); when(reservationDateService.findById(1L)).thenReturn(date); when(reservationTimeService.findById(2L)).thenReturn(time); when(themeService.findById(3L)).thenReturn(theme); when(reservationRepository.existsByDateIdAndTimeIdAndThemeId(1L, 2L, 3L)).thenReturn(true); - when(waitingReservationRepository.existsByNameAndDateIdAndTimeIdAndThemeId("고래", 1L, 2L, 3L)).thenReturn(true); + when(waitingReservationRepository.existsByMemberIdAndDateIdAndTimeIdAndThemeId(1L, 1L, 2L, 3L)).thenReturn(true); assertThatThrownBy(() -> waitingReservationService.createWaitingReservation(request)) .isInstanceOf(RoomescapeException.class) @@ -119,11 +125,13 @@ void setUp() { @Test void 과거_시간에는_예약_대기를_신청할_수_없다() { + Member member = Member.of(1L, "고래"); ReservationDate date = ReservationDate.of(1L, LocalDate.of(2026, 5, 5)); ReservationTime time = ReservationTime.of(2L, LocalTime.of(13, 59)); Theme theme = Theme.of(3L, "공포", "테마 내용", "/themes/scary"); - WaitingReservationCreationRequest request = new WaitingReservationCreationRequest("고래", 1L, 2L, 3L); + WaitingReservationCreationRequest request = new WaitingReservationCreationRequest(1L, 1L, 2L, 3L); + when(memberService.findById(1L)).thenReturn(member); when(reservationDateService.findById(1L)).thenReturn(date); when(reservationTimeService.findById(2L)).thenReturn(time); when(themeService.findById(3L)).thenReturn(theme); @@ -134,23 +142,24 @@ void setUp() { } @Test - void 이름으로_예약_대기와_순번을_조회할_수_있다() { + void 멤버ID로_예약_대기와_순번을_조회할_수_있다() { + Member member = Member.of(2L, "이산"); ReservationDate date = ReservationDate.of(1L, LocalDate.of(2026, 5, 10)); ReservationTime time = ReservationTime.of(2L, LocalTime.of(10, 0)); Theme theme = Theme.of(3L, "공포", "테마 내용", "/themes/scary"); WaitingReservation waiting = WaitingReservation.of( - 10L, "이산", date, time, theme, LocalDateTime.of(2026, 5, 5, 14, 0) + 10L, member, date, time, theme, LocalDateTime.of(2026, 5, 5, 14, 0) ); RankProjection rankProjection = new RankProjection() { public Long getId() { return 10L; } public Long getRank() { return 5L; } }; - when(waitingReservationRepository.findAllByName("이산")).thenReturn(List.of(waiting)); - when(waitingReservationRepository.findRankByName("이산")).thenReturn(List.of(rankProjection)); + when(waitingReservationRepository.findAllByMemberId(2L)).thenReturn(List.of(waiting)); + when(waitingReservationRepository.findRankByMemberId(2L)).thenReturn(List.of(rankProjection)); List result = waitingReservationService - .getWaitingReservationsWithRankByName("이산"); + .getWaitingReservationsWithRankByMemberId(2L); WaitingReservationWithRankResponse response = result.get(0); assertThat(response.id()).isEqualTo(10L); diff --git a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationTest.java b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationTest.java index 93b275fdc9..7b5b4a9854 100644 --- a/src/test/java/roomescape/domain/waitingreservation/WaitingReservationTest.java +++ b/src/test/java/roomescape/domain/waitingreservation/WaitingReservationTest.java @@ -7,58 +7,49 @@ import java.time.LocalDateTime; import java.time.LocalTime; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; +import roomescape.domain.member.Member; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; +import roomescape.support.exception.MemberErrorCode; import roomescape.support.exception.RoomescapeException; class WaitingReservationTest { @Test void 예약_대기가_정상적으로_생성된다() { - // given - String name = "고래"; + Member member = Member.of(1L, "고래"); ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2026, 5, 27)); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(10, 0)); Theme theme = Theme.createWithoutId("공포", "테마 내용", "themes/theme"); LocalDateTime createdAt = LocalDateTime.of(2026, 5, 26, 11, 0); - // when & then - assertThatCode(() -> WaitingReservation.createWithoutId(name, date, time, theme, createdAt)) + assertThatCode(() -> WaitingReservation.createWithoutId(member, date, time, theme, createdAt)) .doesNotThrowAnyException(); } - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" ", " "}) - void 이름이_null이면_예외가_발생한다(String invalidName) { - // given - String name = invalidName; + @Test + void 멤버가_null이면_예외가_발생한다() { + Member member = null; ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2026, 5, 27)); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(10, 0)); Theme theme = Theme.createWithoutId("공포", "테마 내용", "themes/theme"); LocalDateTime createdAt = LocalDateTime.of(2026, 5, 26, 11, 0); - // when & then - assertThatThrownBy(() -> WaitingReservation.createWithoutId(name, date, time, theme, createdAt)) + assertThatThrownBy(() -> WaitingReservation.createWithoutId(member, date, time, theme, createdAt)) .isInstanceOf(RoomescapeException.class) - .hasMessageContaining("예약자 성명 데이터가 유효하지 않습니다."); + .hasMessage(MemberErrorCode.INVALID_MEMBER.getMessage()); } @Test void 생성_시간이_null이면_예외가_발생한다() { - // given - String name = "고래"; + Member member = Member.of(1L, "고래"); ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2026, 5, 27)); ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(10, 0)); Theme theme = Theme.createWithoutId("공포", "테마 내용", "themes/theme"); LocalDateTime createdAt = null; - // when & then - assertThatThrownBy(() -> WaitingReservation.createWithoutId(name, date, time, theme, createdAt)) + assertThatThrownBy(() -> WaitingReservation.createWithoutId(member, date, time, theme, createdAt)) .isInstanceOf(RoomescapeException.class) .hasMessageContaining("생성 시간이 유효하지 않습니다."); } diff --git a/src/test/resources/truncate.sql b/src/test/resources/truncate.sql index 0c2856620d..ea1b75a0f6 100644 --- a/src/test/resources/truncate.sql +++ b/src/test/resources/truncate.sql @@ -1,12 +1,14 @@ SET REFERENTIAL_INTEGRITY FALSE; TRUNCATE TABLE waiting_reservation; TRUNCATE TABLE reservation; +TRUNCATE TABLE member; TRUNCATE TABLE reservation_date; TRUNCATE TABLE reservation_time; TRUNCATE TABLE theme; SET REFERENTIAL_INTEGRITY TRUE; ALTER TABLE waiting_reservation ALTER COLUMN id RESTART WITH 1; ALTER TABLE reservation ALTER COLUMN id RESTART WITH 1; +ALTER TABLE member ALTER COLUMN id RESTART WITH 1; ALTER TABLE reservation_date ALTER COLUMN id RESTART WITH 1; ALTER TABLE reservation_time ALTER COLUMN id RESTART WITH 1; ALTER TABLE theme ALTER COLUMN id RESTART WITH 1; From bf8e7b1d8b3430ca2c378fa8221bd08536970c98 Mon Sep 17 00:00:00 2001 From: rin Date: Thu, 18 Jun 2026 16:38:48 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[2=EB=8B=A8=EA=B3=84]=20:=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/js/times.js | 21 ++++----------------- src/main/resources/templates/times.html | 16 +++++++--------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/main/resources/static/js/times.js b/src/main/resources/static/js/times.js index 5e8de109a1..20fa835c8a 100644 --- a/src/main/resources/static/js/times.js +++ b/src/main/resources/static/js/times.js @@ -298,26 +298,13 @@ document.addEventListener("DOMContentLoaded", () => { const reservationForm = document.getElementById("reservation-form"); const message = document.getElementById("reservation-message"); - const reservationNameInput = document.getElementById("reservation-name"); - const searchNameInput = document.getElementById("search-name"); - - [reservationNameInput, searchNameInput].forEach(input => { - if (input) { - input.addEventListener("input", (e) => { - const value = e.target.value; - if (/\s/.test(value)) { - e.target.value = value.replace(/\s/g, ""); - } - }); - } - }); reservationForm.addEventListener("submit", async (event) => { event.preventDefault(); const formData = new FormData(reservationForm); const payload = { - name: formData.get("name"), + memberId: Number(formData.get("memberId")), themeId: Number(formData.get("themeId")), dateId: Number(formData.get("dateId")), timeId: Number(formData.get("timeId")) @@ -382,14 +369,14 @@ document.addEventListener("DOMContentLoaded", () => { searchForm.addEventListener("submit", async (event) => { event.preventDefault(); - const name = document.getElementById("search-name").value; + const memberId = document.getElementById("search-member-id").value; searchMessage.textContent = ""; searchMessage.className = "form-message"; try { const [reservations, waitingReservations] = await Promise.all([ - fetchJson(`/reservations?name=${encodeURIComponent(name)}`), - fetchJson(`/waiting-reservations?name=${encodeURIComponent(name)}`) + fetchJson(`/reservations-mine?memberId=${memberId}`), + fetchJson(`/waiting-reservations?memberId=${memberId}`) ]); if (reservations.length === 0 && waitingReservations.length === 0) { diff --git a/src/main/resources/templates/times.html b/src/main/resources/templates/times.html index d3fb193b59..521b13ac35 100644 --- a/src/main/resources/templates/times.html +++ b/src/main/resources/templates/times.html @@ -85,10 +85,9 @@

가능한 시간을 선택하세요

예약 정보 입력

테마와 날짜를 선택해 주세요.

- - + +

@@ -102,15 +101,14 @@

예약 정보 입력

CHECK

내 예약 확인

-

예약하실 때 입력하신 이름을 검색해 주세요.

+

멤버 ID로 검색해 주세요.

- - + +