From 7dd3764605fb9308a8ceb13fb9363a60cc960c84 Mon Sep 17 00:00:00 2001 From: Sumin Date: Tue, 12 May 2026 15:11:45 +0900 Subject: [PATCH 01/43] =?UTF-8?q?test:=20Fake=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationServiceTest.java | 149 +++++++++++++----- .../ReservationDateServiceTest.java | 51 ++++-- .../ReservationTimeServiceTest.java | 63 +++++--- .../domain/theme/ThemeServiceTest.java | 56 +++---- .../fake/FakeReservationDateRepository.java | 29 ++-- .../fake/FakeReservationRepository.java | 74 ++++++--- .../fake/FakeReservationTimeRepository.java | 29 ++-- .../support/fake/FakeThemeRepository.java | 34 ++-- 8 files changed, 331 insertions(+), 154 deletions(-) diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index 6a56c1579b..34ab312d7b 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -4,9 +4,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import java.time.Clock; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; @@ -14,6 +18,7 @@ import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; +import roomescape.support.exception.BadRequestException; import roomescape.support.exception.RoomescapeException; import roomescape.support.fake.FakeReservationDateRepository; import roomescape.support.fake.FakeReservationRepository; @@ -22,50 +27,65 @@ class ReservationServiceTest { + private static final ZoneId ZONE_ID = ZoneId.systemDefault(); + private FakeReservationRepository reservationRepository; + private FakeReservationTimeRepository reservationTimeRepository; + private FakeReservationDateRepository reservationDateRepository; + private FakeThemeRepository themeRepository; + + @BeforeEach + void setUp() { + reservationRepository = new FakeReservationRepository(); + reservationTimeRepository = new FakeReservationTimeRepository(); + reservationDateRepository = new FakeReservationDateRepository(); + themeRepository = new FakeThemeRepository(); + } + @Test void 존재하는_예약_시간으로_예약을_생성한다() { // given - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - FakeReservationTimeRepository reservationTimeRepository = new FakeReservationTimeRepository(); - FakeReservationDateRepository reservationDateRepository = new FakeReservationDateRepository(); - FakeThemeRepository themeRepository = new FakeThemeRepository(); - ReservationTime reservationTime = ReservationTime.of(1L, LocalTime.of(10, 0)); - ReservationDate reservationDate = ReservationDate.of(2L, LocalDate.of(2026, 5, 4)); - Theme theme = Theme.of(3L, "공포", "무서운 테마", "theme-url"); - reservationTimeRepository.reservationTime = reservationTime; - reservationDateRepository.reservationDate = reservationDate; - themeRepository.theme = theme; + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = ReservationTime.createWithoutId(LocalTime.of(10, 0)); + ReservationTime savedReservationTime = reservationTimeRepository.save(reservationTime); + ReservationDate reservationDate = ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)); + ReservationDate savedReservationDate = reservationDateRepository.save(reservationDate); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); ReservationService reservationService = new ReservationService( reservationRepository, reservationTimeRepository, reservationDateRepository, - themeRepository + themeRepository, + now + ); + CreateReservationRequest request = new CreateReservationRequest( + "보예", + savedReservationDate.getId(), + savedReservationTime.getId(), + theme.getId() ); - CreateReservationRequest request = new CreateReservationRequest("보예", 2L, 1L, 3L); // when CreateReservationResponse response = reservationService.createReservation(request); // then assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(1L); assertThat(response.name()).isEqualTo("보예"); - assertThat(response.date()).isEqualTo(LocalDate.of(2026, 5, 4)); + assertThat(response.date()).isEqualTo(LocalDate.of(2026, 5, 13)); assertThat(response.time()).isEqualTo(LocalTime.of(10, 0)); assertThat(response.theme().name()).isEqualTo("공포"); - assertThat(reservationRepository.savedReservation.getTime()).isEqualTo(reservationTime); - assertThat(reservationRepository.savedReservation.getTheme()).isEqualTo(theme); }); } @Test void 존재하지_않는_예약_시간으로_예약을_생성하면_예외가_발생한다() { // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationService reservationService = new ReservationService( - new FakeReservationRepository(), - new FakeReservationTimeRepository(), - new FakeReservationDateRepository(), - new FakeThemeRepository() + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now ); CreateReservationRequest request = new CreateReservationRequest("보예", 1L, 1L, 1L); @@ -78,17 +98,24 @@ class ReservationServiceTest { @Test void 존재하지_않는_테마로_예약을_생성하면_예외가_발생한다() { // given - FakeReservationTimeRepository reservationTimeRepository = new FakeReservationTimeRepository(); - FakeReservationDateRepository reservationDateRepository = new FakeReservationDateRepository(); - reservationTimeRepository.reservationTime = ReservationTime.of(1L, LocalTime.of(10, 0)); - reservationDateRepository.reservationDate = ReservationDate.of(2L, LocalDate.of(2026, 5, 4)); + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0))); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13))); ReservationService reservationService = new ReservationService( - new FakeReservationRepository(), + reservationRepository, reservationTimeRepository, reservationDateRepository, - new FakeThemeRepository() + themeRepository, + now + ); + CreateReservationRequest request = new CreateReservationRequest( + "보예", + reservationDate.getId(), + reservationTime.getId(), + 3L ); - CreateReservationRequest request = new CreateReservationRequest("보예", 2L, 1L, 3L); // when & then assertThatThrownBy(() -> reservationService.createReservation(request)) @@ -99,22 +126,28 @@ class ReservationServiceTest { @Test void 예약_목록을_조회한다() { // given - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - FakeReservationTimeRepository reservationTimeRepository = new FakeReservationTimeRepository(); - reservationRepository.findAllResult = List.of( - Reservation.of( - 1L, + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationDate savedReservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운테마", "theme-url")); + reservationRepository.save( + Reservation.createWithoutId( "보예", - ReservationDate.of(3L, LocalDate.of(2026, 5, 4)), - ReservationTime.of(2L, LocalTime.of(10, 0)), - Theme.of(4L, "공포", "무서운 테마", "theme-url") + savedReservationDate, + reservationTime, + theme ) ); ReservationService reservationService = new ReservationService( reservationRepository, reservationTimeRepository, - new FakeReservationDateRepository(), - new FakeThemeRepository() + reservationDateRepository, + themeRepository, + now ); // when @@ -123,13 +156,47 @@ class ReservationServiceTest { // then assertSoftly(softly -> { assertThat(responses).hasSize(1); - assertThat(responses.getFirst().id()).isEqualTo(1L); assertThat(responses.getFirst().name()).isEqualTo("보예"); - assertThat(responses.getFirst().date()).isEqualTo(LocalDate.of(2026, 5, 4)); - assertThat(responses.getFirst().time().id()).isEqualTo(2L); + assertThat(responses.getFirst().date()).isEqualTo(LocalDate.of(2026, 5, 13)); + assertThat(responses.getFirst().time().id()).isEqualTo(reservationTime.getId()); assertThat(responses.getFirst().time().startAt()).isEqualTo(LocalTime.of(10, 0)); - assertThat(responses.getFirst().theme().id()).isEqualTo(4L); + assertThat(responses.getFirst().theme().id()).isEqualTo(theme.getId()); assertThat(responses.getFirst().theme().name()).isEqualTo("공포"); }); } + + @Test + void 오늘보다_이전_날짜는_예약할_수_없다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(14, 0)) + ); + ReservationDate beforeToday = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 10)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + CreateReservationRequest request = new CreateReservationRequest( + "보예", + beforeToday.getId(), + reservationTime.getId(), + theme.getId() + ); + + // when & then + assertThatThrownBy(() -> reservationService.createReservation(request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("예약 날짜는 오늘 이후여야 합니다. 오늘 날짜:" + LocalDate.of(2026, 5, 12)); + } + + private Clock fixedClockAt(LocalDateTime dateTime) { + return Clock.fixed(dateTime.atZone(ZONE_ID).toInstant(), ZONE_ID); + } } diff --git a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java index 6df9d3d0b1..a239e7696a 100644 --- a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java +++ b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java @@ -5,22 +5,34 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import roomescape.domain.reservation.Reservation; import roomescape.domain.reservationdate.dto.AdminReservationDateResponse; import roomescape.domain.reservationdate.dto.CreateReservationDateRequest; import roomescape.domain.reservationdate.dto.CreateReservationDateResponse; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.Theme; import roomescape.support.exception.RoomescapeException; import roomescape.support.fake.FakeReservationDateRepository; import roomescape.support.fake.FakeReservationRepository; class ReservationDateServiceTest { + private FakeReservationRepository reservationRepository; + private FakeReservationDateRepository reservationDateRepository; + + @BeforeEach + void setUp() { + reservationRepository = new FakeReservationRepository(); + reservationDateRepository = new FakeReservationDateRepository(); + } + @Test void 예약_날짜를_생성한다() { // given - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - FakeReservationDateRepository reservationDateRepository = new FakeReservationDateRepository(); ReservationDateService reservationDateService = new ReservationDateService( reservationRepository, reservationDateRepository @@ -30,21 +42,21 @@ class ReservationDateServiceTest { CreateReservationDateResponse response = reservationDateService.createReservationDate( new CreateReservationDateRequest(LocalDate.of(2026, 5, 4)) ); + ReservationDate reservationDate = reservationDateRepository.findById(response.id()).orElseThrow(); // then assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(1L); + assertThat(response.id()).isEqualTo(reservationDate.getId()); assertThat(response.reservationDate()).isEqualTo(LocalDate.of(2026, 5, 4)); - assertThat(reservationDateRepository.savedReservationDate.getDate()).isEqualTo(LocalDate.of(2026, 5, 4)); + assertThat(reservationDate.getDate()).isEqualTo(LocalDate.of(2026, 5, 4)); }); } @Test void 예약_날짜_목록을_조회한다() { // given - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - FakeReservationDateRepository reservationDateRepository = new FakeReservationDateRepository(); - reservationDateRepository.findAllResult = List.of(ReservationDate.of(1L, LocalDate.of(2026, 5, 4))); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); ReservationDateService reservationDateService = new ReservationDateService( reservationRepository, reservationDateRepository @@ -56,7 +68,7 @@ class ReservationDateServiceTest { // then assertSoftly(softly -> { assertThat(responses).hasSize(1); - assertThat(responses.getFirst().id()).isEqualTo(1L); + assertThat(responses.getFirst().id()).isEqualTo(reservationDate.getId()); assertThat(responses.getFirst().reservationDate()).isEqualTo(LocalDate.of(2026, 5, 4)); }); } @@ -64,16 +76,23 @@ class ReservationDateServiceTest { @Test void 이미_예약이_존재하는_날짜는_삭제할_수_없다() { // given - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - reservationRepository.countByReservationDateIdResult = 1; - FakeReservationDateRepository reservationDateRepository = new FakeReservationDateRepository(); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); + reservationRepository.save( + Reservation.createWithoutId( + "보예", + reservationDate, + ReservationTime.of(1L, LocalTime.of(10, 0)), + Theme.of(1L, "공포", "무서운 테마", "theme-url") + ) + ); ReservationDateService reservationDateService = new ReservationDateService( reservationRepository, reservationDateRepository ); // when & then - assertThatThrownBy(() -> reservationDateService.deleteReservationDate(1L)) + assertThatThrownBy(() -> reservationDateService.deleteReservationDate(reservationDate.getId())) .isInstanceOf(RoomescapeException.class) .hasMessage("이미 예약이 존재하는 날짜는 삭제할 수 없습니다."); } @@ -81,17 +100,17 @@ class ReservationDateServiceTest { @Test void 예약이_없는_날짜는_삭제한다() { // given - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - FakeReservationDateRepository reservationDateRepository = new FakeReservationDateRepository(); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); ReservationDateService reservationDateService = new ReservationDateService( reservationRepository, reservationDateRepository ); // when - reservationDateService.deleteReservationDate(1L); + reservationDateService.deleteReservationDate(reservationDate.getId()); // then - assertThat(reservationDateRepository.deletedId).isEqualTo(1L); + assertThat(reservationDateRepository.findById(reservationDate.getId())).isEmpty(); } } diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java index fddee053b2..6937263d8a 100644 --- a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java @@ -2,25 +2,38 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.BDDAssertions.tuple; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import java.time.LocalDate; import java.time.LocalTime; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.dto.CreateTimeRequest; import roomescape.domain.reservationtime.dto.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeResponse; +import roomescape.domain.theme.Theme; import roomescape.support.exception.RoomescapeException; import roomescape.support.fake.FakeReservationRepository; import roomescape.support.fake.FakeReservationTimeRepository; class ReservationTimeServiceTest { + private FakeReservationRepository reservationRepository; + private FakeReservationTimeRepository reservationTimeRepository; + + @BeforeEach + void setUp() { + reservationRepository = new FakeReservationRepository(); + reservationTimeRepository = new FakeReservationTimeRepository(); + } + @Test void 예약_시간을_생성한다() { // given - FakeReservationTimeRepository reservationTimeRepository = new FakeReservationTimeRepository(); - FakeReservationRepository reservationRepository = new FakeReservationRepository(); ReservationTimeService reservationTimeService = new ReservationTimeService( reservationTimeRepository, reservationRepository @@ -30,23 +43,24 @@ class ReservationTimeServiceTest { CreateTimeResponse response = reservationTimeService.createReservationTime( new CreateTimeRequest(LocalTime.of(10, 0)) ); + ReservationTime savedReservationTime = reservationTimeRepository.findById(response.id()).orElseThrow(); // then assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(1L); + assertThat(response.id()).isEqualTo(savedReservationTime.getId()); assertThat(response.startAt()).isEqualTo(LocalTime.of(10, 0)); - assertThat(reservationTimeRepository.savedReservationTime.getStartAt()).isEqualTo(LocalTime.of(10, 0)); + assertThat(savedReservationTime.getStartAt()).isEqualTo(LocalTime.of(10, 0)); }); } @Test void 예약_시간_목록을_조회한다() { // given - FakeReservationTimeRepository reservationTimeRepository = new FakeReservationTimeRepository(); - reservationTimeRepository.findAllResult = List.of(ReservationTime.of(1L, LocalTime.of(10, 0))); + reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(10, 0))); + reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(11, 0))); ReservationTimeService reservationTimeService = new ReservationTimeService( reservationTimeRepository, - new FakeReservationRepository() + reservationRepository ); // when @@ -54,25 +68,37 @@ class ReservationTimeServiceTest { // then assertSoftly(softly -> { - assertThat(responses).hasSize(1); - assertThat(responses.getFirst().id()).isEqualTo(1L); - assertThat(responses.getFirst().startAt()).isEqualTo(LocalTime.of(10, 0)); + assertThat(responses).hasSize(2); + assertThat(responses) + .extracting(ReservationTimeResponse::id, ReservationTimeResponse::startAt) + .containsExactly( + tuple(1L, LocalTime.of(10, 0)), + tuple(2L, LocalTime.of(11, 0)) + ); }); } @Test void 이미_예약이_존재하는_시간은_삭제할_수_없다() { // given - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - reservationRepository.countByTimeIdResult = 1; - FakeReservationTimeRepository reservationTimeRepository = new FakeReservationTimeRepository(); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + reservationRepository.save( + Reservation.createWithoutId( + "보예", + ReservationDate.of(1L, LocalDate.of(2026, 5, 12)), + reservationTime, + Theme.of(1L, "공포", "무서운 테마", "theme-url") + ) + ); ReservationTimeService reservationTimeService = new ReservationTimeService( reservationTimeRepository, reservationRepository ); // when & then - assertThatThrownBy(() -> reservationTimeService.deleteReservationTime(1L)) + assertThatThrownBy(() -> reservationTimeService.deleteReservationTime(reservationTime.getId())) .isInstanceOf(RoomescapeException.class) .hasMessage("이미 예약이 존재하는 시간대는 삭제할 수 없습니다."); } @@ -80,17 +106,18 @@ class ReservationTimeServiceTest { @Test void 예약이_없는_시간은_삭제한다() { // given - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - FakeReservationTimeRepository reservationTimeRepository = new FakeReservationTimeRepository(); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); ReservationTimeService reservationTimeService = new ReservationTimeService( reservationTimeRepository, reservationRepository ); // when - reservationTimeService.deleteReservationTime(1L); + reservationTimeService.deleteReservationTime(reservationTime.getId()); // then - assertThat(reservationTimeRepository.deletedId).isEqualTo(1L); + assertThat(reservationTimeRepository.findById(reservationTime.getId())).isEmpty(); } } diff --git a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java index f044af3ace..520ef0c80f 100644 --- a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import roomescape.domain.theme.dto.AdminThemeResponse; import roomescape.domain.theme.dto.CreateThemeRequest; @@ -14,14 +15,19 @@ class ThemeServiceTest { + private FakeReservationRepository reservationRepository; + private FakeThemeRepository themeRepository; + + @BeforeEach + void setUp() { + reservationRepository = new FakeReservationRepository(); + themeRepository = new FakeThemeRepository(); + } + @Test void 관리자용_테마_목록을_조회한다() { // given - FakeThemeRepository themeRepository = new FakeThemeRepository(); - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - themeRepository.findAllResult = List.of( - Theme.of(1L, "미스터리", "이게 뭘까? 바로바로 추리 테마", "theme/mystery") - ); + themeRepository.save(Theme.createWithoutId("미스터리", "보예의 미스터리", "theme-url")); ThemeService themeService = new ThemeService(themeRepository, reservationRepository); // when @@ -32,19 +38,15 @@ class ThemeServiceTest { assertThat(responses).hasSize(1); assertThat(responses.getFirst().id()).isEqualTo(1L); assertThat(responses.getFirst().name()).isEqualTo("미스터리"); - assertThat(responses.getFirst().content()).isEqualTo("이게 뭘까? 바로바로 추리 테마"); - assertThat(responses.getFirst().url()).isEqualTo("theme/mystery"); + assertThat(responses.getFirst().content()).isEqualTo("보예의 미스터리"); + assertThat(responses.getFirst().url()).isEqualTo("theme-url"); }); } @Test void 사용자용_테마_목록을_조회한다() { // given - FakeThemeRepository themeRepository = new FakeThemeRepository(); - FakeReservationRepository reservationRepository = new FakeReservationRepository(); - themeRepository.findAllResult = List.of( - Theme.of(1L, "미스터리", "이게 뭘까? 바로바로 추리 테마", "theme/mystery") - ); + themeRepository.save(Theme.createWithoutId("미스터리", "보예의 미스터리", "theme-url")); ThemeService themeService = new ThemeService(themeRepository, reservationRepository); // when @@ -55,46 +57,46 @@ class ThemeServiceTest { assertThat(responses).hasSize(1); assertThat(responses.getFirst().id()).isEqualTo(1L); assertThat(responses.getFirst().name()).isEqualTo("미스터리"); - assertThat(responses.getFirst().content()).isEqualTo("이게 뭘까? 바로바로 추리 테마"); - assertThat(responses.getFirst().url()).isEqualTo("theme/mystery"); + assertThat(responses.getFirst().content()).isEqualTo("보예의 미스터리"); + assertThat(responses.getFirst().url()).isEqualTo("theme-url"); }); } @Test void 테마를_생성한다() { // given - FakeThemeRepository themeRepository = new FakeThemeRepository(); - FakeReservationRepository reservationRepository = new FakeReservationRepository(); ThemeService themeService = new ThemeService(themeRepository, reservationRepository); // when CreateThemeResponse response = themeService.createTheme( - new CreateThemeRequest("미스터리", "이게 뭘까? 바로바로 추리 테마", "theme/mystery") + new CreateThemeRequest("미스터리", "보예의 미스터리", "theme-url") ); + Theme theme = themeRepository.findById(response.id()).orElseThrow(); // then assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(1L); + assertThat(response.id()).isEqualTo(theme.getId()); assertThat(response.name()).isEqualTo("미스터리"); - assertThat(response.content()).isEqualTo("이게 뭘까? 바로바로 추리 테마"); - assertThat(response.url()).isEqualTo("theme/mystery"); - assertThat(themeRepository.savedTheme.getName()).isEqualTo("미스터리"); - assertThat(themeRepository.savedTheme.getContent()).isEqualTo("이게 뭘까? 바로바로 추리 테마"); - assertThat(themeRepository.savedTheme.getUrl()).isEqualTo("theme/mystery"); + assertThat(response.content()).isEqualTo("보예의 미스터리"); + assertThat(response.url()).isEqualTo("theme-url"); + assertThat(theme.getName()).isEqualTo("미스터리"); + assertThat(theme.getContent()).isEqualTo("보예의 미스터리"); + assertThat(theme.getUrl()).isEqualTo("theme-url"); }); } @Test void 테마를_삭제한다() { // given - FakeThemeRepository themeRepository = new FakeThemeRepository(); - FakeReservationRepository reservationRepository = new FakeReservationRepository(); + Theme theme = themeRepository.save( + Theme.createWithoutId("공포", "무섭다", "theme-url") + ); ThemeService themeService = new ThemeService(themeRepository, reservationRepository); // when - themeService.deleteTheme(1L); + themeService.deleteTheme(theme.getId()); // then - assertThat(themeRepository.deletedId).isEqualTo(1L); + assertThat(themeRepository.findById(theme.getId())).isEmpty(); } } diff --git a/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java b/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java index 31ec4939f9..f2de75c7b6 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java @@ -1,36 +1,47 @@ package roomescape.support.fake; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; public class FakeReservationDateRepository implements ReservationDateRepository { - public ReservationDate reservationDate; - public ReservationDate savedReservationDate; - public List findAllResult = List.of(); - public Long deletedId; + private final Map storage = new LinkedHashMap<>(); + private long sequence = 1L; @Override public Optional findById(Long id) { - return Optional.ofNullable(reservationDate); + return Optional.ofNullable(storage.get(id)); } @Override public List findAll() { - return findAllResult; + return new ArrayList<>(storage.values()); } @Override public ReservationDate save(ReservationDate reservationDate) { - savedReservationDate = reservationDate; - return ReservationDate.of(1L, reservationDate.getDate()); + Long id = reservationDate.getId(); + if (id == null) { + id = sequence++; + } else { + sequence = Math.max(sequence, id + 1); + } + ReservationDate savedReservationDate = ReservationDate.of(id, reservationDate.getDate()); + storage.put(id, savedReservationDate); + return savedReservationDate; } @Override public int deleteById(Long id) { - deletedId = id; + ReservationDate removedReservationDate = storage.remove(id); + if (removedReservationDate == null) { + return 0; + } return 1; } } diff --git a/src/test/java/roomescape/support/fake/FakeReservationRepository.java b/src/test/java/roomescape/support/fake/FakeReservationRepository.java index ddde77f5ac..70b5a64fc6 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationRepository.java @@ -1,54 +1,77 @@ package roomescape.support.fake; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.theme.Theme; public class FakeReservationRepository implements ReservationRepository { - public Reservation savedReservation; - public List findAllResult = List.of(); - public int countByTimeIdResult; - public int countByReservationDateIdResult; - public int countByThemeIdResult; + private final Map storage = new LinkedHashMap<>(); + private long sequence = 1L; @Override public Reservation save(Reservation reservation) { - savedReservation = reservation; - return Reservation.of( - 1L, - reservation.getName(), - reservation.getDate(), - reservation.getTime(), - reservation.getTheme() - ); + Long id = reservation.getId(); + if (id == null) { + id = sequence++; + } else { + sequence = Math.max(sequence, id + 1); + } + Reservation savedReservation = Reservation.createWithId(id, reservation); + storage.put(id, savedReservation); + return savedReservation; } @Override public List findAll() { - return findAllResult; + return new ArrayList<>(storage.values()); } @Override public int deleteById(Long id) { - return 0; + Reservation removedReservation = storage.remove(id); + if (removedReservation == null) { + return 0; + } + return 1; } @Override public int countByTimeId(Long timeId) { - return countByTimeIdResult; + int count = 0; + for (Reservation value : storage.values()) { + if (value.getTime().getId().equals(timeId)) { + count++; + } + } + return count; } @Override public int countByReservationDateId(Long dateId) { - return countByReservationDateIdResult; + int count = 0; + for (Reservation value : storage.values()) { + if (value.getDate().getId().equals(dateId)) { + count++; + } + } + return count; } @Override public List findReservedTimes(Long themeId, Long dateId) { - return List.of(); + List reservedTimeIds = new ArrayList<>(); + for (Reservation reservation : storage.values()) { + if (reservation.getTheme().getId().equals(themeId) && reservation.getDate().getId().equals(dateId)) { + reservedTimeIds.add(reservation.getTime().getId()); + } + } + return reservedTimeIds; } @Override @@ -57,7 +80,18 @@ public List findPopularThemes(int rankLimit, LocalDate startDay, LocalDat } @Override - public int countByThemeId(Long id) { - return countByThemeIdResult; + public int countByThemeId(Long themeId) { + int count = 0; + for (Reservation reservation : storage.values()) { + if (reservation.getTheme().getId().equals(themeId)) { + count++; + } + } + return count; + } + + public void insert(Reservation reservation) { + storage.put(reservation.getId(), reservation); + sequence = Math.max(sequence, reservation.getId() + 1); } } diff --git a/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java b/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java index de138f080a..ac35d2a60f 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java @@ -1,36 +1,47 @@ package roomescape.support.fake; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.reservationtime.ReservationTimeRepository; public class FakeReservationTimeRepository implements ReservationTimeRepository { - public ReservationTime reservationTime; - public ReservationTime savedReservationTime; - public List findAllResult = List.of(); - public Long deletedId; + private final Map storage = new LinkedHashMap<>(); + private long sequence = 1L; @Override public Optional findById(Long id) { - return Optional.ofNullable(reservationTime); + return Optional.ofNullable(storage.get(id)); } @Override public ReservationTime save(ReservationTime reservationTime) { - savedReservationTime = reservationTime; - return ReservationTime.of(1L, reservationTime.getStartAt()); + Long id = reservationTime.getId(); + if (id == null) { + id = sequence++; + } else { + sequence = Math.max(sequence, id + 1); + } + ReservationTime savedReservationTime = ReservationTime.of(id, reservationTime.getStartAt()); + storage.put(id, savedReservationTime); + return savedReservationTime; } @Override public List findAll() { - return findAllResult; + return new ArrayList<>(storage.values()); } @Override public int deleteById(Long id) { - deletedId = id; + ReservationTime removedReservationTime = storage.remove(id); + if (removedReservationTime == null) { + return 0; + } return 1; } } diff --git a/src/test/java/roomescape/support/fake/FakeThemeRepository.java b/src/test/java/roomescape/support/fake/FakeThemeRepository.java index 8d46629247..8b7a17467c 100644 --- a/src/test/java/roomescape/support/fake/FakeThemeRepository.java +++ b/src/test/java/roomescape/support/fake/FakeThemeRepository.java @@ -1,41 +1,47 @@ package roomescape.support.fake; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import roomescape.domain.theme.Theme; import roomescape.domain.theme.ThemeRepository; public class FakeThemeRepository implements ThemeRepository { - public List findAllResult = List.of(); - public Theme theme; - public Theme savedTheme; - public Long deletedId; + private final Map storage = new LinkedHashMap<>(); + private long sequence = 1L; @Override public List findAll() { - return findAllResult; + return new ArrayList<>(storage.values()); } @Override public Optional findById(Long id) { - if (theme != null) { - return Optional.of(theme); - } - return findAllResult.stream() - .filter(candidate -> candidate.getId().equals(id)) - .findFirst(); + return Optional.ofNullable(storage.get(id)); } @Override public Theme save(Theme theme) { - savedTheme = theme; - return Theme.of(1L, theme.getName(), theme.getContent(), theme.getUrl()); + Long id = theme.getId(); + if (id == null) { + id = sequence++; + } else { + sequence = Math.max(sequence, id + 1); + } + Theme savedTheme = Theme.of(id, theme.getName(), theme.getContent(), theme.getUrl()); + storage.put(id, savedTheme); + return savedTheme; } @Override public int deleteById(Long id) { - deletedId = id; + Theme removedTheme = storage.remove(id); + if (removedTheme == null) { + return 0; + } return 1; } } From c884f2dd7c755dae7d1ad1096b9b721889ff15b7 Mon Sep 17 00:00:00 2001 From: Sumin Date: Tue, 12 May 2026 15:35:03 +0900 Subject: [PATCH 02/43] =?UTF-8?q?refactor:=20=EC=A7=80=EB=82=9C=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EB=B0=8F=20=EC=8B=9C=EA=B0=84=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EB=B6=88=EA=B0=80=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/config/TimeConfig.java | 14 +++ .../reservation/ReservationService.java | 28 ++++- .../reservationdate/ReservationDate.java | 8 ++ .../reservationtime/ReservationTime.java | 4 + .../exception/ReservationDateErrorCode.java | 1 + .../exception/ReservationTimeErrorCode.java | 1 + .../reservation/ReservationServiceTest.java | 115 ++++++++++++++++++ .../fake/FakeReservationRepository.java | 5 + 8 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/main/java/roomescape/config/TimeConfig.java diff --git a/src/main/java/roomescape/config/TimeConfig.java b/src/main/java/roomescape/config/TimeConfig.java new file mode 100644 index 0000000000..18823bee03 --- /dev/null +++ b/src/main/java/roomescape/config/TimeConfig.java @@ -0,0 +1,14 @@ +package roomescape.config; + +import java.time.Clock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TimeConfig { + + @Bean + public Clock clock() { + return Clock.systemDefaultZone(); + } +} diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index c4018d37ed..672f2467d7 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -1,5 +1,8 @@ package roomescape.domain.reservation; +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,6 +16,7 @@ import roomescape.domain.reservationtime.ReservationTimeRepository; import roomescape.domain.theme.Theme; import roomescape.domain.theme.ThemeRepository; +import roomescape.support.exception.BadRequestException; import roomescape.support.exception.NotFoundException; import roomescape.support.exception.ReservationDateErrorCode; import roomescape.support.exception.ReservationTimeErrorCode; @@ -27,15 +31,18 @@ public class ReservationService { private final ReservationTimeRepository reservationTimeRepository; private final ReservationDateRepository reservationDateRepository; private final ThemeRepository themeRepository; + private final Clock clock; public CreateReservationResponse createReservation(CreateReservationRequest request) { ReservationTime reservationTime = reservationTimeRepository.findById(request.timeId()) .orElseThrow(() -> new NotFoundException(ReservationTimeErrorCode.RESERVATION_TIME_NOT_EXIST)); ReservationDate reservationDate = reservationDateRepository.findById(request.dateId()) .orElseThrow(() -> new NotFoundException(ReservationDateErrorCode.RESERVATION_DATE_NOT_EXIST)); + checkReservationDateAndReservationTime(reservationDate, reservationTime); Theme theme = themeRepository.findById(request.themeId()) .orElseThrow(() -> new NotFoundException(ThemeErrorCode.THEME_NOT_EXIST)); - Reservation savedReservation = reservationRepository.save(request.toEntity(reservationDate, reservationTime, theme)); + Reservation savedReservation = reservationRepository.save( + request.toEntity(reservationDate, reservationTime, theme)); return CreateReservationResponse.from(savedReservation); } @@ -51,4 +58,23 @@ public void deleteReservation(Long id) { log.warn("이미 삭제된 예약 삭제 요청이 들어왔습니다. reservationId={}", id); } } + + private void checkReservationDateAndReservationTime( + ReservationDate reservationDate, + ReservationTime reservationTime + ) { + LocalDate today = LocalDate.now(clock); + if (reservationDate.isBefore(today)) { + throw new BadRequestException( + ReservationDateErrorCode.RESERVATION_DATE_MUST_BE_TODAY_OR_LATER, + LocalDate.now(clock) + ); + } + if (reservationDate.isSame(today) && reservationTime.isBefore(LocalTime.now(clock))) { + throw new BadRequestException( + ReservationTimeErrorCode.RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER, + LocalTime.now(clock) + ); + } + } } diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDate.java b/src/main/java/roomescape/domain/reservationdate/ReservationDate.java index d761a88932..4e8477b4a3 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDate.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDate.java @@ -24,4 +24,12 @@ public static ReservationDate createWithoutId(LocalDate reservationDate) { reservationDate ); } + + public boolean isBefore(LocalDate compareDate) { + return date.isBefore(compareDate); + } + + public boolean isSame(LocalDate compareDate) { + return date.isEqual(compareDate); + } } diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java index 1ecb57b6dd..bc88ab0a66 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java @@ -36,4 +36,8 @@ private static void validate(LocalTime startAt) { throw new BadRequestException(ReservationTimeErrorCode.INVALID_RESERVATION_TIME); } } + + public boolean isBefore(LocalTime compareTime) { + return startAt.isBefore(compareTime); + } } diff --git a/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java b/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java index dbf8be10e2..099a909114 100644 --- a/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java @@ -7,6 +7,7 @@ public enum ReservationDateErrorCode implements ErrorCode { RESERVATION_DATE_NOT_EXIST("존재하지 않는 날짜 입니다."), RESERVATION_DATE_IN_USE("이미 예약이 존재하는 날짜는 삭제할 수 없습니다."), + RESERVATION_DATE_MUST_BE_TODAY_OR_LATER("예약 날짜는 오늘 이후여야 합니다. 오늘 날짜:%s"), ; private final String message; diff --git a/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java b/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java index 5f5586e6e1..03e0b93598 100644 --- a/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java @@ -9,6 +9,7 @@ public enum ReservationTimeErrorCode implements ErrorCode { INVALID_RESERVATION_TIME_FORMAT("시간은 HH:MM 형식이어야 합니다."), RESERVATION_TIME_NOT_EXIST("존재하지 않는 예약 시간대 입니다."), RESERVATION_TIME_IN_USE("이미 예약이 존재하는 시간대는 삭제할 수 없습니다."), + RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER("예약 시간은 현재 이후여야 합니다. 현재 시각:%s"), ; private final String message; diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index 34ab312d7b..18c2f2eac9 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -196,6 +196,121 @@ void setUp() { .hasMessage("예약 날짜는 오늘 이후여야 합니다. 오늘 날짜:" + LocalDate.of(2026, 5, 12)); } + @Test + void 오늘_예약일_경우_현재_시간_이전은_예약할_수_없다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime beforeNow = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(12, 59)) + ); + ReservationDate today = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 12)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + CreateReservationRequest request = new CreateReservationRequest( + "보예", + today.getId(), + beforeNow.getId(), + theme.getId() + ); + + // when & then + assertThatThrownBy(() -> reservationService.createReservation(request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("예약 시간은 현재 이후여야 합니다. 현재 시각:" + LocalTime.of(13, 0)); + } + + @Test + void 오늘_예약이지만_현재_시간은_예약할_수_있다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime nowTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(13, 0)) + ); + ReservationDate today = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 12)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + CreateReservationRequest request = new CreateReservationRequest( + "보예", + today.getId(), + nowTime.getId(), + theme.getId() + ); + + // when + CreateReservationResponse response = reservationService.createReservation(request); + Reservation reservation = reservationRepository.findById(response.id()).orElseThrow(); + + // then + assertSoftly(softly -> { + assertThat(response.id()).isEqualTo(reservation.getId()); + assertThat(response.date()).isEqualTo(LocalDate.of(2026, 5, 12)); + assertThat(response.time()).isEqualTo(LocalTime.of(13, 0)); + assertThat(response.name()).isEqualTo("보예"); + assertThat(response.theme().name()).isEqualTo("공포"); + assertThat(response.theme().content()).isEqualTo("무서운 테마"); + assertThat(response.theme().url()).isEqualTo("theme-url"); + } + ); + } + + @Test + void 날짜가_오늘_이후이고_현재_시간보다_이전이면_정상_예약_된다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + ReservationDate today = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + CreateReservationRequest request = new CreateReservationRequest( + "보예", + today.getId(), + reservationTime.getId(), + theme.getId() + ); + + // when + CreateReservationResponse response = reservationService.createReservation(request); + Reservation reservation = reservationRepository.findById(response.id()).orElseThrow(); + + // then + assertSoftly(softly -> { + assertThat(response.id()).isEqualTo(reservation.getId()); + assertThat(response.date()).isEqualTo(LocalDate.of(2026, 5, 13)); + assertThat(response.time()).isEqualTo(LocalTime.of(10, 0)); + assertThat(response.name()).isEqualTo("보예"); + assertThat(response.theme().name()).isEqualTo("공포"); + assertThat(response.theme().content()).isEqualTo("무서운 테마"); + assertThat(response.theme().url()).isEqualTo("theme-url"); + } + ); + } + private Clock fixedClockAt(LocalDateTime dateTime) { return Clock.fixed(dateTime.atZone(ZONE_ID).toInstant(), ZONE_ID); } diff --git a/src/test/java/roomescape/support/fake/FakeReservationRepository.java b/src/test/java/roomescape/support/fake/FakeReservationRepository.java index 70b5a64fc6..a9c78c7ff5 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationRepository.java @@ -5,6 +5,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.theme.Theme; @@ -14,6 +15,10 @@ public class FakeReservationRepository implements ReservationRepository { private final Map storage = new LinkedHashMap<>(); private long sequence = 1L; + public Optional findById(Long id) { + return Optional.ofNullable(storage.get(id)); + } + @Override public Reservation save(Reservation reservation) { Long id = reservation.getId(); From ccdd2de6c065106df46b844432b33673abc4d6b8 Mon Sep 17 00:00:00 2001 From: Sumin Date: Tue, 12 May 2026 16:07:57 +0900 Subject: [PATCH 03/43] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=98=88=EC=95=BD=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JdbcReservationRepository.java | 19 +++++++++++- .../reservation/ReservationRepository.java | 2 ++ .../reservation/ReservationService.java | 8 +++++ .../exception/ReservationErrorCode.java | 1 + .../reservation/ReservationServiceTest.java | 30 +++++++++++++++++++ .../fake/FakeReservationRepository.java | 13 ++++++-- 6 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java index 31e0c746b5..3b80e82312 100644 --- a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java @@ -85,7 +85,12 @@ select count(*) from reservation where theme_id = ? """; - ; + private static final String COUNT_RESERVATION_BY_TIME_AND_DATE_AND_THEME = + """ + select count(*) + from reservation r + where time_id = ? and date_id = ? and theme_id = ? + """; private final JdbcTemplate jdbcTemplate; @@ -151,6 +156,18 @@ public int countByThemeId(Long themeId) { return count; } + @Override + public boolean existsReservation(Long timeId, Long dateId, Long themeId) { + Integer count = jdbcTemplate.queryForObject( + COUNT_RESERVATION_BY_TIME_AND_DATE_AND_THEME, + Integer.class, + timeId, + themeId, + dateId + ); + return count != null && count > 0; + } + private RowMapper reservationRowMapper() { return (rs, rowNum) -> Reservation.of( rs.getLong(COLUMN_ID), diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index 0e64f42a51..200664b7a2 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -21,4 +21,6 @@ public interface ReservationRepository { List findPopularThemes(int rankLimit, LocalDate startDay, LocalDate today); int countByThemeId(Long id); + + boolean existsReservation(Long timeId, Long dateId, Long themeId); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 672f2467d7..0e64cfefb3 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -19,6 +19,7 @@ import roomescape.support.exception.BadRequestException; import roomescape.support.exception.NotFoundException; import roomescape.support.exception.ReservationDateErrorCode; +import roomescape.support.exception.ReservationErrorCode; import roomescape.support.exception.ReservationTimeErrorCode; import roomescape.support.exception.ThemeErrorCode; @@ -41,6 +42,7 @@ public CreateReservationResponse createReservation(CreateReservationRequest requ checkReservationDateAndReservationTime(reservationDate, reservationTime); Theme theme = themeRepository.findById(request.themeId()) .orElseThrow(() -> new NotFoundException(ThemeErrorCode.THEME_NOT_EXIST)); + checkDuplicated(reservationTime, reservationDate, theme); Reservation savedReservation = reservationRepository.save( request.toEntity(reservationDate, reservationTime, theme)); return CreateReservationResponse.from(savedReservation); @@ -77,4 +79,10 @@ private void checkReservationDateAndReservationTime( ); } } + + private void checkDuplicated(ReservationTime reservationTime, ReservationDate reservationDate, Theme theme) { + if (reservationRepository.existsReservation(reservationTime.getId(), reservationDate.getId(), theme.getId())) { + throw new BadRequestException(ReservationErrorCode.DUPLICATED_RESERVATION); + } + } } diff --git a/src/main/java/roomescape/support/exception/ReservationErrorCode.java b/src/main/java/roomescape/support/exception/ReservationErrorCode.java index 97d11b0a34..26f935ec2a 100644 --- a/src/main/java/roomescape/support/exception/ReservationErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationErrorCode.java @@ -8,6 +8,7 @@ public enum ReservationErrorCode implements ErrorCode { INVALID_RESERVATION_NAME("이름은 비어 있을 수 없습니다."), INVALID_RESERVATION_DATE("날짜는 필수입니다."), RESERVATION_NOT_FOUND("존재하지 않는 예약건 입니다"), + DUPLICATED_RESERVATION("중복 예약입니다. 예약 정보를 다시 확인해주세요."), ; private final String message; diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index 18c2f2eac9..aa85a6c36b 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -311,6 +311,36 @@ void setUp() { ); } + @Test + void 중복된_예약은_예외가_발생한다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = ReservationTime.createWithoutId(LocalTime.of(10, 0)); + ReservationTime savedReservationTime = reservationTimeRepository.save(reservationTime); + ReservationDate reservationDate = ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)); + ReservationDate savedReservationDate = reservationDateRepository.save(reservationDate); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + CreateReservationRequest request = new CreateReservationRequest( + "보예", + savedReservationDate.getId(), + savedReservationTime.getId(), + theme.getId() + ); + reservationService.createReservation(request); + + // when & then + assertThatThrownBy(() -> reservationService.createReservation(request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("중복 예약입니다. 예약 정보를 다시 확인해주세요."); + } + private Clock fixedClockAt(LocalDateTime dateTime) { return Clock.fixed(dateTime.atZone(ZONE_ID).toInstant(), ZONE_ID); } diff --git a/src/test/java/roomescape/support/fake/FakeReservationRepository.java b/src/test/java/roomescape/support/fake/FakeReservationRepository.java index a9c78c7ff5..bbdb4f2243 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationRepository.java @@ -95,8 +95,15 @@ public int countByThemeId(Long themeId) { return count; } - public void insert(Reservation reservation) { - storage.put(reservation.getId(), reservation); - sequence = Math.max(sequence, reservation.getId() + 1); + @Override + public boolean existsReservation(Long timeId, Long dateId, Long themeId) { + for (Reservation reservation : storage.values()) { + if (timeId.equals(reservation.getTime().getId()) + && dateId.equals(reservation.getDate().getId()) + && themeId.equals(reservation.getTheme().getId())) { + return true; + } + } + return false; } } From 96c1806ba3888e7ed4780953809240b80a13f8d4 Mon Sep 17 00:00:00 2001 From: Sumin Date: Tue, 12 May 2026 16:18:19 +0900 Subject: [PATCH 04/43] =?UTF-8?q?refactor:=20AdminReservationController=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationController.java | 26 ------------ .../reservation/ReservationService.java | 2 +- .../admin/AdminReservationController.java | 40 +++++++++++++++++++ 3 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index 54e3210894..b45a0abd62 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -1,36 +1,19 @@ package roomescape.domain.reservation; -import jakarta.servlet.http.HttpServletRequest; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.support.auth.AdminRequestValidator; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; -import roomescape.domain.reservation.dto.ReservationResponse; @RestController @RequiredArgsConstructor public class ReservationController { private final ReservationService reservationService; - private final AdminRequestValidator validator; - - @GetMapping("/admin/reservations") - public ResponseEntity> getAllReservation(HttpServletRequest request) { - if (validator.isUnauthorized(request)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - List response = reservationService.getAllReservations(); - return ResponseEntity.ok(response); - } @PostMapping("/reservations") public ResponseEntity createReservation(@RequestBody CreateReservationRequest request) { @@ -38,13 +21,4 @@ public ResponseEntity createReservation(@RequestBody CreateReservationResponse response = reservationService.createReservation(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } - - @DeleteMapping("/admin/reservations/{id}") - public ResponseEntity deleteReservation(HttpServletRequest request, @PathVariable Long id) { - if (validator.isUnauthorized(request)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - reservationService.deleteReservation(id); - return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); - } } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 0e64cfefb3..ef4c853667 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -54,7 +54,7 @@ public List getAllReservations() { .toList(); } - public void deleteReservation(Long id) { + public void deleteReservationByAdmin(Long id) { int deletedCount = reservationRepository.deleteById(id); if (deletedCount == 0) { log.warn("이미 삭제된 예약 삭제 요청이 들어왔습니다. reservationId={}", id); diff --git a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java new file mode 100644 index 0000000000..d7c5067ca4 --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java @@ -0,0 +1,40 @@ +package roomescape.domain.reservation.admin; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import roomescape.domain.reservation.ReservationService; +import roomescape.domain.reservation.dto.ReservationResponse; +import roomescape.support.auth.AdminRequestValidator; + +@RestController +@RequiredArgsConstructor +public class AdminReservationController { + + private final ReservationService reservationService; + private final AdminRequestValidator validator; + + @GetMapping("/admin/reservations") + public ResponseEntity> getAllReservation(HttpServletRequest request) { + if (validator.isUnauthorized(request)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + List response = reservationService.getAllReservations(); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/admin/reservations/{id}") + public ResponseEntity deleteReservation(HttpServletRequest request, @PathVariable Long id) { + if (validator.isUnauthorized(request)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + reservationService.deleteReservationByAdmin(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } +} From 853acd8ef52fe2b90de4ed345f7fe1c69acd353f Mon Sep 17 00:00:00 2001 From: Sumin Date: Tue, 12 May 2026 17:13:33 +0900 Subject: [PATCH 05/43] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JdbcReservationRepository.java | 27 ++++++- .../reservation/ReservationController.java | 9 +++ .../reservation/ReservationRepository.java | 2 + .../reservation/ReservationService.java | 8 +- .../admin/AdminReservationController.java | 2 +- .../{ => admin}/dto/ReservationResponse.java | 2 +- .../dto/UserReservationResponse.java | 80 +++++++++++++++++++ .../reservation/ReservationServiceTest.java | 65 ++++++++++++++- .../fake/FakeReservationRepository.java | 17 ++++ 9 files changed, 203 insertions(+), 9 deletions(-) rename src/main/java/roomescape/domain/reservation/{ => admin}/dto/ReservationResponse.java (96%) create mode 100644 src/main/java/roomescape/domain/reservation/dto/UserReservationResponse.java diff --git a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java index 3b80e82312..c0f589cd58 100644 --- a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java @@ -85,13 +85,27 @@ select count(*) from reservation where theme_id = ? """; - private static final String COUNT_RESERVATION_BY_TIME_AND_DATE_AND_THEME = + private static final String COUNT_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL = """ select count(*) from reservation r where time_id = ? and date_id = ? and theme_id = ? """; + private static final String FIND_BY_NAME_SQL = + """ + select r.id, r.name, + rd.id as date_id, rd.date, + rt.id as time_id, rt.start_at, + th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url + from reservation 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.date desc, rt.start_at desc, r.id desc + """; + private final JdbcTemplate jdbcTemplate; @Override @@ -159,15 +173,20 @@ public int countByThemeId(Long themeId) { @Override public boolean existsReservation(Long timeId, Long dateId, Long themeId) { Integer count = jdbcTemplate.queryForObject( - COUNT_RESERVATION_BY_TIME_AND_DATE_AND_THEME, + COUNT_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL, Integer.class, timeId, - themeId, - dateId + dateId, + themeId ); return count != null && count > 0; } + @Override + public List findByName(String name) { + return jdbcTemplate.query(FIND_BY_NAME_SQL, reservationRowMapper(), name); + } + private RowMapper reservationRowMapper() { return (rs, rowNum) -> Reservation.of( rs.getLong(COLUMN_ID), diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index b45a0abd62..c5d6936971 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -3,11 +3,14 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; +import roomescape.domain.reservation.dto.UserReservationResponse; @RestController @RequiredArgsConstructor @@ -21,4 +24,10 @@ public ResponseEntity createReservation(@RequestBody CreateReservationResponse response = reservationService.createReservation(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } + + @GetMapping("/reservations/{name}") + public ResponseEntity getUserReservations(@PathVariable String name) { + UserReservationResponse response = reservationService.getUserReservations(name); + 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 200664b7a2..4b6ac7bf6a 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -23,4 +23,6 @@ public interface ReservationRepository { int countByThemeId(Long id); boolean existsReservation(Long timeId, Long dateId, Long themeId); + + List findByName(String name); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index ef4c853667..3ed3f39a89 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -7,9 +7,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import roomescape.domain.reservation.admin.dto.ReservationResponse; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; -import roomescape.domain.reservation.dto.ReservationResponse; +import roomescape.domain.reservation.dto.UserReservationResponse; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; import roomescape.domain.reservationtime.ReservationTime; @@ -85,4 +86,9 @@ private void checkDuplicated(ReservationTime reservationTime, ReservationDate re throw new BadRequestException(ReservationErrorCode.DUPLICATED_RESERVATION); } } + + public UserReservationResponse getUserReservations(String name) { + List reservations = reservationRepository.findByName(name); + return UserReservationResponse.of(name, reservations); + } } diff --git a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java index d7c5067ca4..52e725bdef 100644 --- a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java +++ b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import roomescape.domain.reservation.ReservationService; -import roomescape.domain.reservation.dto.ReservationResponse; +import roomescape.domain.reservation.admin.dto.ReservationResponse; import roomescape.support.auth.AdminRequestValidator; @RestController diff --git a/src/main/java/roomescape/domain/reservation/dto/ReservationResponse.java b/src/main/java/roomescape/domain/reservation/admin/dto/ReservationResponse.java similarity index 96% rename from src/main/java/roomescape/domain/reservation/dto/ReservationResponse.java rename to src/main/java/roomescape/domain/reservation/admin/dto/ReservationResponse.java index 02884b3128..1b7d0ec83d 100644 --- a/src/main/java/roomescape/domain/reservation/dto/ReservationResponse.java +++ b/src/main/java/roomescape/domain/reservation/admin/dto/ReservationResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservation.dto; +package roomescape.domain.reservation.admin.dto; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalDate; diff --git a/src/main/java/roomescape/domain/reservation/dto/UserReservationResponse.java b/src/main/java/roomescape/domain/reservation/dto/UserReservationResponse.java new file mode 100644 index 0000000000..8aeb9630bc --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/dto/UserReservationResponse.java @@ -0,0 +1,80 @@ +package roomescape.domain.reservation.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservationdate.ReservationDate; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.Theme; + +public record UserReservationResponse( + String name, + List reservation +) { + + public static UserReservationResponse of(String name, List reservation) { + return new UserReservationResponse( + name, + reservation.stream() + .map(ReservationPayload::from) + .toList() + ); + } + + public record ReservationPayload( + Long reservationId, + ReservationDatePayload date, + ReservationTimePayload time, + ThemePayload theme + ) { + + public static ReservationPayload from(Reservation reservation) { + return new ReservationPayload( + reservation.getId(), + ReservationDatePayload.from(reservation.getDate()), + ReservationTimePayload.from(reservation.getTime()), + ThemePayload.from(reservation.getTheme()) + ); + } + } + + public record ReservationDatePayload( + Long id, + LocalDate startWhen + ) { + + public static ReservationDatePayload from(ReservationDate date) { + return new ReservationDatePayload(date.getId(), date.getDate()); + } + } + + public record ReservationTimePayload( + Long id, + @JsonFormat(pattern = "HH:mm") + LocalTime startAt + ) { + + public static ReservationTimePayload from(ReservationTime reservationTime) { + return new ReservationTimePayload(reservationTime.getId(), reservationTime.getStartAt()); + } + } + + public record ThemePayload( + Long id, + String name, + String content, + String url + ) { + + public static ThemePayload from(Theme theme) { + return new ThemePayload( + theme.getId(), + theme.getName(), + theme.getContent(), + theme.getUrl() + ); + } + } +} diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index aa85a6c36b..d03494bb0c 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.BDDAssertions.tuple; import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.time.Clock; @@ -12,9 +13,10 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import roomescape.domain.reservation.admin.dto.ReservationResponse; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; -import roomescape.domain.reservation.dto.ReservationResponse; +import roomescape.domain.reservation.dto.UserReservationResponse; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; @@ -124,7 +126,7 @@ void setUp() { } @Test - void 예약_목록을_조회한다() { + void 예약_목록을_전체_조회한다() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationDate savedReservationDate = reservationDateRepository.save( @@ -165,6 +167,65 @@ void setUp() { }); } + @Test + void 사용자가_이름으로_예약을_조회한다() { + // given + String name = "보예짱"; + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationDate firstReservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + ReservationDate secondReservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 14)) + ); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운테마", "theme-url")); + reservationRepository.save( + Reservation.createWithoutId( + name, + secondReservationDate, + reservationTime, + theme + ) + ); + reservationRepository.save( + Reservation.createWithoutId( + name, + firstReservationDate, + reservationTime, + theme + ) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + + // when + UserReservationResponse userReservations = reservationService.getUserReservations(name); + + // then + assertSoftly(softly -> { + assertThat(userReservations.reservation().size()).isEqualTo(2); + assertThat(userReservations.name()).isEqualTo("보예짱"); + assertThat(userReservations.reservation()) + .extracting( + reservationPayload -> reservationPayload.date().startWhen(), + reservationPayload -> reservationPayload.time().startAt(), + reservationPayload -> reservationPayload.theme().name() + ) + .containsExactly( + tuple(LocalDate.of(2026, 5, 14), LocalTime.of(10, 0), "공포"), + tuple(LocalDate.of(2026, 5, 13), LocalTime.of(10, 0), "공포") + ); + }); + } + @Test void 오늘보다_이전_날짜는_예약할_수_없다() { // given diff --git a/src/test/java/roomescape/support/fake/FakeReservationRepository.java b/src/test/java/roomescape/support/fake/FakeReservationRepository.java index bbdb4f2243..6578bc2203 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationRepository.java @@ -2,6 +2,7 @@ import java.time.LocalDate; import java.util.ArrayList; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -106,4 +107,20 @@ public boolean existsReservation(Long timeId, Long dateId, Long themeId) { } return false; } + + @Override + public List findByName(String name) { + List reservations = new ArrayList<>(storage.values().stream() + .filter(reservation -> name.equals(reservation.getName())) + .toList()); + reservations.sort(latestReservationFirst()); + return reservations; + } + + private Comparator latestReservationFirst() { + return Comparator.comparing((Reservation reservation) -> reservation.getDate().getDate()) + .reversed() + .thenComparing(reservation -> reservation.getTime().getStartAt(), Comparator.reverseOrder()) + .thenComparing(Reservation::getId, Comparator.reverseOrder()); + } } From a3a8fac6574c0fc404f272b8394bf9ad12ece09a Mon Sep 17 00:00:00 2001 From: Sumin Date: Tue, 12 May 2026 19:30:45 +0900 Subject: [PATCH 06/43] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JdbcReservationRepository.java | 20 +++- .../reservation/ReservationController.java | 7 ++ .../reservation/ReservationRepository.java | 3 + .../reservation/ReservationService.java | 47 ++++++-- .../exception/ReservationDateErrorCode.java | 1 + .../exception/ReservationTimeErrorCode.java | 1 + src/main/resources/data.sql | 1 + .../reservation/ReservationServiceTest.java | 103 ++++++++++++++++++ 8 files changed, 172 insertions(+), 11 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java index c0f589cd58..9a891e73c3 100644 --- a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java @@ -4,6 +4,7 @@ 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; @@ -91,7 +92,6 @@ select count(*) from reservation r where time_id = ? and date_id = ? and theme_id = ? """; - private static final String FIND_BY_NAME_SQL = """ select r.id, r.name, @@ -105,6 +105,18 @@ select count(*) where r.name = ? order by rd.date desc, rt.start_at desc, r.id desc """; + private static final String FIND_BY_ID_SQL = + """ + select r.id, r.name, + rd.id as date_id, rd.date, + rt.id as time_id, rt.start_at, + th.id as theme_id, th.name as theme_name, th.content as theme_content, th.url as theme_url + from reservation 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 final JdbcTemplate jdbcTemplate; @@ -187,6 +199,12 @@ public List findByName(String name) { return jdbcTemplate.query(FIND_BY_NAME_SQL, reservationRowMapper(), name); } + @Override + public Optional findById(Long id) { + List result = jdbcTemplate.query(FIND_BY_ID_SQL, reservationRowMapper(), id); + return result.stream().findFirst(); + } + private RowMapper reservationRowMapper() { return (rs, rowNum) -> Reservation.of( rs.getLong(COLUMN_ID), diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index c5d6936971..f3193ad84f 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -30,4 +31,10 @@ public ResponseEntity getUserReservations(@PathVariable UserReservationResponse response = reservationService.getUserReservations(name); return ResponseEntity.ok(response); } + + @DeleteMapping("/reservations/{id}") + public ResponseEntity deleteUserReservation(@PathVariable Long id) { + reservationService.deleteUserReservation(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } } diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index 4b6ac7bf6a..bf118198d6 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -2,6 +2,7 @@ import java.time.LocalDate; import java.util.List; +import java.util.Optional; import roomescape.domain.theme.Theme; public interface ReservationRepository { @@ -25,4 +26,6 @@ public interface ReservationRepository { boolean existsReservation(Long timeId, Long dateId, Long themeId); List findByName(String name); + + Optional findById(Long id); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 3ed3f39a89..f35f2aac7c 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -62,22 +62,25 @@ public void deleteReservationByAdmin(Long id) { } } + public void deleteUserReservation(Long id) { + Reservation reservation = reservationRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ReservationErrorCode.RESERVATION_NOT_FOUND)); + validateUserCanDeleteReservation(reservation); + reservationRepository.deleteById(id); + } + + //TODO: 메서드명 변경 private void checkReservationDateAndReservationTime( ReservationDate reservationDate, ReservationTime reservationTime ) { LocalDate today = LocalDate.now(clock); - if (reservationDate.isBefore(today)) { - throw new BadRequestException( - ReservationDateErrorCode.RESERVATION_DATE_MUST_BE_TODAY_OR_LATER, - LocalDate.now(clock) - ); + LocalTime now = LocalTime.now(clock); + if (isPastDate(reservationDate, today)) { + throw new BadRequestException(ReservationDateErrorCode.RESERVATION_DATE_MUST_BE_TODAY_OR_LATER, today); } - if (reservationDate.isSame(today) && reservationTime.isBefore(LocalTime.now(clock))) { - throw new BadRequestException( - ReservationTimeErrorCode.RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER, - LocalTime.now(clock) - ); + if (isPastTimeToday(reservationDate, reservationTime, today, now)) { + throw new BadRequestException(ReservationTimeErrorCode.RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER, now); } } @@ -87,6 +90,30 @@ private void checkDuplicated(ReservationTime reservationTime, ReservationDate re } } + private void validateUserCanDeleteReservation(Reservation reservation) { + LocalDate today = LocalDate.now(clock); + LocalTime now = LocalTime.now(clock); + if (isPastDate(reservation.getDate(), today)) { + throw new BadRequestException(ReservationDateErrorCode.PAST_RESERVATION_DATE_CANNOT_BE_DELETED, today); + } + if (isPastTimeToday(reservation.getDate(), reservation.getTime(), today, now)) { + throw new BadRequestException(ReservationTimeErrorCode.PAST_RESERVATION_TiME_CANNOT_BE_DELETED, now); + } + } + + private boolean isPastDate(ReservationDate reservationDate, LocalDate today) { + return reservationDate.isBefore(today); + } + + private boolean isPastTimeToday( + ReservationDate reservationDate, + ReservationTime reservationTime, + LocalDate today, + LocalTime now + ) { + return reservationDate.isSame(today) && reservationTime.isBefore(now); + } + public UserReservationResponse getUserReservations(String name) { List reservations = reservationRepository.findByName(name); return UserReservationResponse.of(name, reservations); diff --git a/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java b/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java index 099a909114..170c42ab28 100644 --- a/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java @@ -8,6 +8,7 @@ public enum ReservationDateErrorCode implements ErrorCode { RESERVATION_DATE_NOT_EXIST("존재하지 않는 날짜 입니다."), RESERVATION_DATE_IN_USE("이미 예약이 존재하는 날짜는 삭제할 수 없습니다."), RESERVATION_DATE_MUST_BE_TODAY_OR_LATER("예약 날짜는 오늘 이후여야 합니다. 오늘 날짜:%s"), + PAST_RESERVATION_DATE_CANNOT_BE_DELETED("예전 예약은 삭제할 수 없습니다. 오늘 날짜:%s"), ; private final String message; diff --git a/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java b/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java index 03e0b93598..4b12751d46 100644 --- a/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java @@ -10,6 +10,7 @@ public enum ReservationTimeErrorCode implements ErrorCode { RESERVATION_TIME_NOT_EXIST("존재하지 않는 예약 시간대 입니다."), RESERVATION_TIME_IN_USE("이미 예약이 존재하는 시간대는 삭제할 수 없습니다."), RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER("예약 시간은 현재 이후여야 합니다. 현재 시각:%s"), + PAST_RESERVATION_TiME_CANNOT_BE_DELETED("현재보다 이전 시간 예약을 삭제할 수 없습니다. 현재 시각:%s"), ; private final String message; diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index dcca1197af..8a1f779a14 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -32,6 +32,7 @@ VALUES ('공포', '오금이 저리는 공포입니다.', '/themes/scary'), INSERT INTO reservation (name, date_id, time_id, theme_id) VALUES ('보예', 1, 1, 1), + ('보예', 9, 1, 1), ('이산', 1, 2, 1), ('나무', 2, 1, 2), ('피즈', 2, 3, 2), diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index d03494bb0c..6ca142fd01 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -402,6 +402,109 @@ void setUp() { .hasMessage("중복 예약입니다. 예약 정보를 다시 확인해주세요."); } + @Test + void 사용자는_미래_예약을_삭제할_수_있다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, reservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + + // when + reservationService.deleteUserReservation(savedReservation.getId()); + + // then + assertThat(reservationRepository.findById(savedReservation.getId())).isEmpty(); + } + + @Test + void 사용자는_이미_시간이_지난_예약을_삭제할_수_없다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(12, 59)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 12)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, reservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + + // when & then + assertThatThrownBy(() -> reservationService.deleteUserReservation(savedReservation.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage("현재보다 이전 시간 예약을 삭제할 수 없습니다. 현재 시각:" + LocalTime.of(13, 0)); + } + + @Test + void 사용자는_이미_날짜가_지난_예약을_삭제할_수_없다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(12, 59)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 11)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, reservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + + // when & then + assertThatThrownBy(() -> reservationService.deleteUserReservation(savedReservation.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage("예전 예약은 삭제할 수 없습니다. 오늘 날짜:" + LocalDate.of(2026, 5, 12)); + } + + @Test + void 사용자가_존재하지_않는_예약을_삭제하면_예외가_발생한다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + + // when & then + assertThatThrownBy(() -> reservationService.deleteUserReservation(1L)) + .isInstanceOf(RoomescapeException.class) + .hasMessage("존재하지 않는 예약건 입니다"); + } + private Clock fixedClockAt(LocalDateTime dateTime) { return Clock.fixed(dateTime.atZone(ZONE_ID).toInstant(), ZONE_ID); } From 75541fa40a07f8a320c32871af0e7aeb5e6f892d Mon Sep 17 00:00:00 2001 From: Sumin Date: Tue, 12 May 2026 19:32:29 +0900 Subject: [PATCH 07/43] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=20=EC=A1=B0=ED=9A=8C=20api=20url=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/ReservationController.java | 5 +++-- .../domain/reservation/ReservationService.java | 15 +++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index f3193ad84f..7b641c8322 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; @@ -26,8 +27,8 @@ public ResponseEntity createReservation(@RequestBody return ResponseEntity.status(HttpStatus.CREATED).body(response); } - @GetMapping("/reservations/{name}") - public ResponseEntity getUserReservations(@PathVariable String name) { + @GetMapping("/reservations") + public ResponseEntity getUserReservations(@RequestParam String name) { UserReservationResponse response = reservationService.getUserReservations(name); return ResponseEntity.ok(response); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index f35f2aac7c..b51eb658e9 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -40,7 +40,7 @@ public CreateReservationResponse createReservation(CreateReservationRequest requ .orElseThrow(() -> new NotFoundException(ReservationTimeErrorCode.RESERVATION_TIME_NOT_EXIST)); ReservationDate reservationDate = reservationDateRepository.findById(request.dateId()) .orElseThrow(() -> new NotFoundException(ReservationDateErrorCode.RESERVATION_DATE_NOT_EXIST)); - checkReservationDateAndReservationTime(reservationDate, reservationTime); + validateReservationScheduleToCreate(reservationDate, reservationTime); Theme theme = themeRepository.findById(request.themeId()) .orElseThrow(() -> new NotFoundException(ThemeErrorCode.THEME_NOT_EXIST)); checkDuplicated(reservationTime, reservationDate, theme); @@ -55,6 +55,11 @@ public List getAllReservations() { .toList(); } + public UserReservationResponse getUserReservations(String name) { + List reservations = reservationRepository.findByName(name); + return UserReservationResponse.of(name, reservations); + } + public void deleteReservationByAdmin(Long id) { int deletedCount = reservationRepository.deleteById(id); if (deletedCount == 0) { @@ -69,8 +74,7 @@ public void deleteUserReservation(Long id) { reservationRepository.deleteById(id); } - //TODO: 메서드명 변경 - private void checkReservationDateAndReservationTime( + private void validateReservationScheduleToCreate( ReservationDate reservationDate, ReservationTime reservationTime ) { @@ -113,9 +117,4 @@ private boolean isPastTimeToday( ) { return reservationDate.isSame(today) && reservationTime.isBefore(now); } - - public UserReservationResponse getUserReservations(String name) { - List reservations = reservationRepository.findByName(name); - return UserReservationResponse.of(name, reservations); - } } From 55b2aa87ec1954131401a254b2ab37db0738df78 Mon Sep 17 00:00:00 2001 From: Sumin Date: Tue, 12 May 2026 19:47:19 +0900 Subject: [PATCH 08/43] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=ED=8C=85=20=EB=88=84=EB=9D=BD=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/support/exception/ErrorResponse.java | 6 +++++- .../support/exception/GlobalExceptionHandler.java | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/roomescape/support/exception/ErrorResponse.java b/src/main/java/roomescape/support/exception/ErrorResponse.java index c1596dc001..5776da2b87 100644 --- a/src/main/java/roomescape/support/exception/ErrorResponse.java +++ b/src/main/java/roomescape/support/exception/ErrorResponse.java @@ -8,9 +8,13 @@ public record ErrorResponse( String message ) { + public static ResponseEntity of(HttpStatus httpStatus, RoomescapeException exception) { + return ResponseEntity.status(httpStatus) + .body(new ErrorResponse(exception.getErrorCode().getCode(), exception.getMessage())); + } + public static ResponseEntity of(HttpStatus httpStatus, ErrorCode errorCode) { return ResponseEntity.status(httpStatus) .body(new ErrorResponse(errorCode.getCode(), errorCode.getMessage())); } - } diff --git a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java index 20c118ffff..b863105443 100644 --- a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java @@ -10,22 +10,22 @@ public class GlobalExceptionHandler { @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(BadRequestException exception) { - return ErrorResponse.of(HttpStatus.BAD_REQUEST, exception.getErrorCode()); + return ErrorResponse.of(HttpStatus.BAD_REQUEST, exception); } @ExceptionHandler(NotFoundException.class) public ResponseEntity handleNotFoundException(NotFoundException exception) { - return ErrorResponse.of(HttpStatus.NOT_FOUND, exception.getErrorCode()); + return ErrorResponse.of(HttpStatus.NOT_FOUND, exception); } @ExceptionHandler(ConflictException.class) public ResponseEntity handleConflictException(ConflictException exception) { - return ErrorResponse.of(HttpStatus.CONFLICT, exception.getErrorCode()); + return ErrorResponse.of(HttpStatus.CONFLICT, exception); } @ExceptionHandler(InternalServerException.class) public ResponseEntity handleInternalServerException(InternalServerException exception) { - return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, exception.getErrorCode()); + return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, exception); } @ExceptionHandler(Exception.class) From 5a9b56ef5d11a86563a7dcc9ed80896adb3ce1ac Mon Sep 17 00:00:00 2001 From: Sumin Date: Wed, 13 May 2026 16:49:47 +0900 Subject: [PATCH 09/43] =?UTF-8?q?feat:=20=EC=98=88=EC=95=BD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JdbcReservationRepository.java | 41 +++ .../reservation/ReservationController.java | 11 + .../reservation/ReservationRepository.java | 4 + .../reservation/ReservationService.java | 68 ++++- .../dto/UpdateReservationRequest.java | 11 + .../JdbcReservationDateRepository.java | 8 + .../ReservationDateRepository.java | 3 + .../JdbcReservationTimeRepository.java | 8 + .../ReservationTimeRepository.java | 3 + .../exception/ReservationErrorCode.java | 1 + .../reservation/ReservationServiceTest.java | 259 ++++++++++++++++++ .../fake/FakeReservationDateRepository.java | 8 + .../fake/FakeReservationRepository.java | 23 ++ .../fake/FakeReservationTimeRepository.java | 8 + 14 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservation/dto/UpdateReservationRequest.java diff --git a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java index 9a891e73c3..a45e1f95e5 100644 --- a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java @@ -92,6 +92,12 @@ select count(*) from reservation r where time_id = ? and date_id = ? and theme_id = ? """; + private static final String COUNT_OTHER_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL = + """ + select count(*) + from reservation r + where r.id <> ? and time_id = ? and date_id = ? and theme_id = ? + """; private static final String FIND_BY_NAME_SQL = """ select r.id, r.name, @@ -117,6 +123,12 @@ select count(*) join theme th on r.theme_id = th.id where r.id = ? """; + private static final String UPDATE_SQL = + """ + update reservation + set name = ?, date_id = ?, time_id = ?, theme_id = ? + where id = ? + """; private final JdbcTemplate jdbcTemplate; @@ -194,6 +206,19 @@ public boolean existsReservation(Long timeId, Long dateId, Long themeId) { return count != null && count > 0; } + @Override + public boolean existsOtherReservation(Long id, Long timeId, Long dateId, Long themeId) { + Integer count = jdbcTemplate.queryForObject( + COUNT_OTHER_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL, + Integer.class, + id, + timeId, + dateId, + themeId + ); + return count != null && count > 0; + } + @Override public List findByName(String name) { return jdbcTemplate.query(FIND_BY_NAME_SQL, reservationRowMapper(), name); @@ -205,6 +230,22 @@ public Optional findById(Long id) { return result.stream().findFirst(); } + @Override + public Optional update(Long id, Reservation withoutId) { + int updatedCount = jdbcTemplate.update( + UPDATE_SQL, + withoutId.getName(), + withoutId.getDate().getId(), + withoutId.getTime().getId(), + withoutId.getTheme().getId(), + id + ); + if (updatedCount == 0) { + return Optional.empty(); + } + return findById(id); + } + private RowMapper reservationRowMapper() { return (rs, rowNum) -> Reservation.of( rs.getLong(COLUMN_ID), diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index 7b641c8322..fd8b41fb3b 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -5,6 +5,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; +import roomescape.domain.reservation.dto.UpdateReservationRequest; import roomescape.domain.reservation.dto.UserReservationResponse; @RestController @@ -38,4 +40,13 @@ public ResponseEntity deleteUserReservation(@PathVariable Long id) { reservationService.deleteUserReservation(id); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } + + @PatchMapping("/reservations/{id}") + public ResponseEntity updateReservation( + @PathVariable Long id, + @RequestBody UpdateReservationRequest request + ) { + reservationService.updateReservation(id, request); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } } diff --git a/src/main/java/roomescape/domain/reservation/ReservationRepository.java b/src/main/java/roomescape/domain/reservation/ReservationRepository.java index bf118198d6..42047b9ee2 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/ReservationRepository.java @@ -25,7 +25,11 @@ public interface ReservationRepository { boolean existsReservation(Long timeId, Long dateId, Long themeId); + boolean existsOtherReservation(Long id, Long timeId, Long dateId, Long themeId); + List findByName(String name); Optional findById(Long id); + + Optional update(Long id, Reservation withoutId); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index b51eb658e9..5e0a4c6c3a 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -7,9 +7,11 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import roomescape.domain.reservation.admin.dto.ReservationResponse; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; +import roomescape.domain.reservation.dto.UpdateReservationRequest; import roomescape.domain.reservation.dto.UserReservationResponse; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationdate.ReservationDateRepository; @@ -26,6 +28,7 @@ @Slf4j @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class ReservationService { @@ -35,6 +38,7 @@ public class ReservationService { private final ThemeRepository themeRepository; private final Clock clock; + @Transactional public CreateReservationResponse createReservation(CreateReservationRequest request) { ReservationTime reservationTime = reservationTimeRepository.findById(request.timeId()) .orElseThrow(() -> new NotFoundException(ReservationTimeErrorCode.RESERVATION_TIME_NOT_EXIST)); @@ -43,7 +47,7 @@ public CreateReservationResponse createReservation(CreateReservationRequest requ validateReservationScheduleToCreate(reservationDate, reservationTime); Theme theme = themeRepository.findById(request.themeId()) .orElseThrow(() -> new NotFoundException(ThemeErrorCode.THEME_NOT_EXIST)); - checkDuplicated(reservationTime, reservationDate, theme); + validateDuplicated(reservationTime, reservationDate, theme); Reservation savedReservation = reservationRepository.save( request.toEntity(reservationDate, reservationTime, theme)); return CreateReservationResponse.from(savedReservation); @@ -60,6 +64,7 @@ public UserReservationResponse getUserReservations(String name) { return UserReservationResponse.of(name, reservations); } + @Transactional public void deleteReservationByAdmin(Long id) { int deletedCount = reservationRepository.deleteById(id); if (deletedCount == 0) { @@ -67,6 +72,7 @@ public void deleteReservationByAdmin(Long id) { } } + @Transactional public void deleteUserReservation(Long id) { Reservation reservation = reservationRepository.findById(id) .orElseThrow(() -> new NotFoundException(ReservationErrorCode.RESERVATION_NOT_FOUND)); @@ -74,6 +80,26 @@ public void deleteUserReservation(Long id) { reservationRepository.deleteById(id); } + @Transactional + public void updateReservation(Long id, UpdateReservationRequest request) { + Reservation reservation = reservationRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ReservationErrorCode.RESERVATION_NOT_FOUND)); + ReservationTime reservationTime = reservation.getTime(); + ReservationDate reservationDate = reservation.getDate(); + reservationTime = getReservationTime(request, reservationTime); + reservationDate = getReservationDate(request, reservationDate); + validateReservationScheduleToCreate(reservationDate, reservationTime); + validateDuplicatedWithOther(id, reservationTime, reservationDate, reservation.getTheme()); + Reservation updatedReservation = Reservation.createWithoutId( + reservation.getName(), + reservationDate, + reservationTime, + reservation.getTheme() + ); + reservationRepository.update(id, updatedReservation) + .orElseThrow(() -> new NotFoundException(ReservationErrorCode.RESERVATION_NOT_FOUND)); + } + private void validateReservationScheduleToCreate( ReservationDate reservationDate, ReservationTime reservationTime @@ -88,8 +114,28 @@ private void validateReservationScheduleToCreate( } } - private void checkDuplicated(ReservationTime reservationTime, ReservationDate reservationDate, Theme theme) { - if (reservationRepository.existsReservation(reservationTime.getId(), reservationDate.getId(), theme.getId())) { + private void validateDuplicated(ReservationTime reservationTime, ReservationDate reservationDate, Theme theme) { + if (isExistReservation(reservationTime, reservationDate, theme)) { + throw new BadRequestException(ReservationErrorCode.DUPLICATED_RESERVATION); + } + } + + private boolean isExistReservation(ReservationTime reservationTime, ReservationDate reservationDate, Theme theme) { + return reservationRepository.existsReservation(reservationTime.getId(), reservationDate.getId(), theme.getId()); + } + + private void validateDuplicatedWithOther( + Long id, + ReservationTime reservationTime, + ReservationDate reservationDate, + Theme theme + ) { + if (reservationRepository.existsOtherReservation( + id, + reservationTime.getId(), + reservationDate.getId(), + theme.getId() + )) { throw new BadRequestException(ReservationErrorCode.DUPLICATED_RESERVATION); } } @@ -117,4 +163,20 @@ private boolean isPastTimeToday( ) { return reservationDate.isSame(today) && reservationTime.isBefore(now); } + + private ReservationDate getReservationDate(UpdateReservationRequest request, ReservationDate reservationDate) { + if (request.startWhen() != null) { + reservationDate = reservationDateRepository.findByDate(request.startWhen()) + .orElseThrow(() -> new NotFoundException(ReservationDateErrorCode.RESERVATION_DATE_NOT_EXIST)); + } + return reservationDate; + } + + private ReservationTime getReservationTime(UpdateReservationRequest request, ReservationTime reservationTime) { + if (request.startAt() != null) { + reservationTime = reservationTimeRepository.findByStartAt(request.startAt()) + .orElseThrow(() -> new NotFoundException(ReservationTimeErrorCode.RESERVATION_TIME_NOT_EXIST)); + } + return reservationTime; + } } diff --git a/src/main/java/roomescape/domain/reservation/dto/UpdateReservationRequest.java b/src/main/java/roomescape/domain/reservation/dto/UpdateReservationRequest.java new file mode 100644 index 0000000000..76aaba3d29 --- /dev/null +++ b/src/main/java/roomescape/domain/reservation/dto/UpdateReservationRequest.java @@ -0,0 +1,11 @@ +package roomescape.domain.reservation.dto; + +import java.time.LocalDate; +import java.time.LocalTime; + +public record UpdateReservationRequest( + LocalDate startWhen, + LocalTime startAt +) { + +} diff --git a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java index 188cbc62f2..7133af6352 100644 --- a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java +++ b/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java @@ -3,6 +3,7 @@ import java.sql.Date; import java.sql.PreparedStatement; import java.sql.Statement; +import java.time.LocalDate; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -23,6 +24,7 @@ public class JdbcReservationDateRepository implements ReservationDateRepository private static final String INSERT_SQL = "insert into reservation_date(`date`) values (?)"; private static final String FIND_BY_ID_SQL = "select id, `date` from reservation_date where id = ?"; + private static final String FIND_BY_DATE_SQL = "select id, `date` from reservation_date where `date` = ?"; private static final String FIND_ALL_SQL = "select id, `date` from reservation_date order by id"; private static final String DELETE_BY_ID_SQL = "delete from reservation_date where id = ?"; @@ -59,6 +61,12 @@ public int deleteById(Long id) { return jdbcTemplate.update(DELETE_BY_ID_SQL, id); } + @Override + public Optional findByDate(LocalDate startWhen) { + List result = jdbcTemplate.query(FIND_BY_DATE_SQL, reservationDateRowMapper(), startWhen); + return result.stream().findFirst(); + } + private RowMapper reservationDateRowMapper() { return (rs, rowNum) -> ReservationDate.of( rs.getLong(COLUMN_ID), diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java index 487ce2cebf..7f245d2125 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateRepository.java @@ -1,5 +1,6 @@ package roomescape.domain.reservationdate; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -12,4 +13,6 @@ public interface ReservationDateRepository { ReservationDate save(ReservationDate reservationDate); int deleteById(Long id); + + Optional findByDate(LocalDate startWhen); } diff --git a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java index 3895af0f60..59479a2879 100644 --- a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java +++ b/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java @@ -3,6 +3,7 @@ import java.sql.PreparedStatement; import java.sql.Statement; import java.sql.Time; +import java.time.LocalTime; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -24,6 +25,7 @@ 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 FIND_BY_TIME_SQL = "select id, start_at from reservation_time where start_at = ?"; private static final String DELETE_BY_ID_SQL = "delete from reservation_time where id = ?"; private final JdbcTemplate jdbcTemplate; @@ -59,6 +61,12 @@ public int deleteById(Long id) { return jdbcTemplate.update(DELETE_BY_ID_SQL, id); } + @Override + public Optional findByStartAt(LocalTime startAt) { + List result = jdbcTemplate.query(FIND_BY_TIME_SQL, reservationTimeRowMapper(), startAt); + return result.stream().findFirst(); + } + private RowMapper reservationTimeRowMapper() { return (rs, rowNum) -> ReservationTime.of( rs.getLong(COLUMN_ID), diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java index 77f773a1ea..c9c4b55eb2 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeRepository.java @@ -1,5 +1,6 @@ package roomescape.domain.reservationtime; +import java.time.LocalTime; import java.util.List; import java.util.Optional; @@ -12,4 +13,6 @@ public interface ReservationTimeRepository { Optional findById(Long id); int deleteById(Long id); + + Optional findByStartAt(LocalTime startAt); } diff --git a/src/main/java/roomescape/support/exception/ReservationErrorCode.java b/src/main/java/roomescape/support/exception/ReservationErrorCode.java index 26f935ec2a..aed73775bc 100644 --- a/src/main/java/roomescape/support/exception/ReservationErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationErrorCode.java @@ -9,6 +9,7 @@ public enum ReservationErrorCode implements ErrorCode { INVALID_RESERVATION_DATE("날짜는 필수입니다."), RESERVATION_NOT_FOUND("존재하지 않는 예약건 입니다"), DUPLICATED_RESERVATION("중복 예약입니다. 예약 정보를 다시 확인해주세요."), + INVALID_RESERVATION_UPDATE_REQUEST("수정할 예약 날짜 또는 시간을 입력해주세요."), ; private final String message; diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index 6ca142fd01..e4b31aaeae 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -16,6 +16,7 @@ import roomescape.domain.reservation.admin.dto.ReservationResponse; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; +import roomescape.domain.reservation.dto.UpdateReservationRequest; import roomescape.domain.reservation.dto.UserReservationResponse; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; @@ -505,6 +506,264 @@ void setUp() { .hasMessage("존재하지 않는 예약건 입니다"); } + @Test + void 예약_날짜와_시간을_수정한다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime beforeReservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + ReservationTime afterReservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(15, 0)) + ); + ReservationDate beforeReservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + ReservationDate afterReservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 14)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", beforeReservationDate, beforeReservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + UpdateReservationRequest request = new UpdateReservationRequest( + afterReservationDate.getDate(), + afterReservationTime.getStartAt() + ); + + // when + reservationService.updateReservation(savedReservation.getId(), request); + + // then + Reservation updatedReservation = reservationRepository.findById(savedReservation.getId()).orElseThrow(); + assertSoftly(softly -> { + assertThat(updatedReservation.getDate().getDate()).isEqualTo(LocalDate.of(2026, 5, 14)); + assertThat(updatedReservation.getTime().getStartAt()).isEqualTo(LocalTime.of(15, 0)); + assertThat(updatedReservation.getName()).isEqualTo("보예"); + assertThat(updatedReservation.getTheme().getName()).isEqualTo("공포"); + }); + } + + @Test + void 예약_시간만_수정한다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime beforeReservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + ReservationTime afterReservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(15, 0)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, beforeReservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + UpdateReservationRequest request = new UpdateReservationRequest(null, afterReservationTime.getStartAt()); + + // when + reservationService.updateReservation(savedReservation.getId(), request); + Reservation updatedReservation = reservationRepository.findById(savedReservation.getId()).orElseThrow(); + + // then + assertSoftly(softly -> { + assertThat(updatedReservation.getDate().getDate()).isEqualTo(LocalDate.of(2026, 5, 13)); + assertThat(updatedReservation.getTime().getStartAt()).isEqualTo(LocalTime.of(15, 0)); + }); + } + + @Test + void 존재하지_않는_예약을_수정하면_예외가_발생한다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + UpdateReservationRequest request = new UpdateReservationRequest(LocalDate.of(2026, 5, 13), LocalTime.of(10, 0)); + + // when & then + assertThatThrownBy(() -> reservationService.updateReservation(1L, request)) + .isInstanceOf(RoomescapeException.class) + .hasMessage("존재하지 않는 예약건 입니다"); + } + + @Test + void 존재하지_않는_예약_날짜로_수정하면_예외가_발생한다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, reservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + UpdateReservationRequest request = new UpdateReservationRequest(LocalDate.of(2026, 5, 14), null); + + // when & then + assertThatThrownBy(() -> reservationService.updateReservation(savedReservation.getId(), request)) + .isInstanceOf(RoomescapeException.class) + .hasMessage("존재하지 않는 날짜 입니다."); + } + + @Test + void 존재하지_않는_예약_시간으로_수정하면_예외가_발생한다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, reservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + UpdateReservationRequest request = new UpdateReservationRequest(null, LocalTime.of(15, 0)); + + // when & then + assertThatThrownBy(() -> reservationService.updateReservation(savedReservation.getId(), request)) + .isInstanceOf(RoomescapeException.class) + .hasMessage("존재하지 않는 예약 시간대 입니다."); + } + + @Test + void 오늘보다_이전_날짜로_예약을_수정할_수_없다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(15, 0)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + ReservationDate beforeToday = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 11)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, reservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + UpdateReservationRequest request = new UpdateReservationRequest(beforeToday.getDate(), null); + + // when & then + assertThatThrownBy(() -> reservationService.updateReservation(savedReservation.getId(), request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("예약 날짜는 오늘 이후여야 합니다. 오늘 날짜:" + LocalDate.of(2026, 5, 12)); + } + + @Test + void 오늘_예약을_현재_시간보다_이전으로_수정할_수_없다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(15, 0)) + ); + ReservationTime beforeNow = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(12, 59)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 12)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, reservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + UpdateReservationRequest request = new UpdateReservationRequest(null, beforeNow.getStartAt()); + + // when & then + assertThatThrownBy(() -> reservationService.updateReservation(savedReservation.getId(), request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("예약 시간은 현재 이후여야 합니다. 현재 시각:" + LocalTime.of(13, 0)); + } + + @Test + void 중복된_예약으로_수정하면_예외가_발생한다() { + // given + Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); + ReservationTime reservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + ReservationTime otherReservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(15, 0)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 13)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + Reservation savedReservation = reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, reservationTime, theme) + ); + reservationRepository.save( + Reservation.createWithoutId("파랑", reservationDate, otherReservationTime, theme) + ); + ReservationService reservationService = new ReservationService( + reservationRepository, + reservationTimeRepository, + reservationDateRepository, + themeRepository, + now + ); + UpdateReservationRequest request = new UpdateReservationRequest(null, otherReservationTime.getStartAt()); + + // when & then + assertThatThrownBy(() -> reservationService.updateReservation(savedReservation.getId(), request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("중복 예약입니다. 예약 정보를 다시 확인해주세요."); + } + private Clock fixedClockAt(LocalDateTime dateTime) { return Clock.fixed(dateTime.atZone(ZONE_ID).toInstant(), ZONE_ID); } diff --git a/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java b/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java index f2de75c7b6..3fb44cd073 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationDateRepository.java @@ -1,5 +1,6 @@ package roomescape.support.fake; +import java.time.LocalDate; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -44,4 +45,11 @@ public int deleteById(Long id) { } return 1; } + + @Override + public Optional findByDate(LocalDate startWhen) { + return storage.values().stream() + .filter(reservationDate -> startWhen.equals(reservationDate.getDate())) + .findFirst(); + } } diff --git a/src/test/java/roomescape/support/fake/FakeReservationRepository.java b/src/test/java/roomescape/support/fake/FakeReservationRepository.java index 6578bc2203..d7b814d353 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationRepository.java @@ -108,6 +108,19 @@ public boolean existsReservation(Long timeId, Long dateId, Long themeId) { return false; } + @Override + public boolean existsOtherReservation(Long id, Long timeId, Long dateId, Long themeId) { + for (Reservation reservation : storage.values()) { + if (!id.equals(reservation.getId()) + && timeId.equals(reservation.getTime().getId()) + && dateId.equals(reservation.getDate().getId()) + && themeId.equals(reservation.getTheme().getId())) { + return true; + } + } + return false; + } + @Override public List findByName(String name) { List reservations = new ArrayList<>(storage.values().stream() @@ -117,6 +130,16 @@ public List findByName(String name) { return reservations; } + @Override + public Optional update(Long id, Reservation withoutId) { + if (!storage.containsKey(id)) { + return Optional.empty(); + } + Reservation updatedReservation = Reservation.createWithId(id, withoutId); + storage.put(id, updatedReservation); + return Optional.of(updatedReservation); + } + private Comparator latestReservationFirst() { return Comparator.comparing((Reservation reservation) -> reservation.getDate().getDate()) .reversed() diff --git a/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java b/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java index ac35d2a60f..8ab8759081 100644 --- a/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java +++ b/src/test/java/roomescape/support/fake/FakeReservationTimeRepository.java @@ -1,5 +1,6 @@ package roomescape.support.fake; +import java.time.LocalTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -44,4 +45,11 @@ public int deleteById(Long id) { } return 1; } + + @Override + public Optional findByStartAt(LocalTime startAt) { + return storage.values().stream() + .filter(reservationTime -> startAt.equals(reservationTime.getStartAt())) + .findFirst(); + } } From a1bc6eccb17fd949697b270adc8b5227a3a4a66f Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 14 May 2026 12:23:00 +0900 Subject: [PATCH 10/43] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=EB=90=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/css/times.css | 145 ++++++++++++- src/main/resources/static/js/times.js | 264 +++++++++++++++++++++++- src/main/resources/templates/times.html | 21 ++ 3 files changed, 427 insertions(+), 3 deletions(-) diff --git a/src/main/resources/static/css/times.css b/src/main/resources/static/css/times.css index 60ae33c02c..9e7d04ef14 100644 --- a/src/main/resources/static/css/times.css +++ b/src/main/resources/static/css/times.css @@ -35,7 +35,8 @@ a { } button, -input { +input, +select { font: inherit; } @@ -345,6 +346,131 @@ input { border-color: var(--blue); } +.form-message.success { + color: #047857; +} + +.form-message.error { + color: #dc2626; +} + +.reservation-lookup-form { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 20px; +} + +.lookup-controls { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; +} + +.lookup-controls input, +.reservation-edit-form select { + min-height: 46px; + padding: 0 14px; + border: 1px solid #cfd8e6; + border-radius: 12px; + background: #fff; + color: var(--text); + outline: none; +} + +.lookup-controls input:focus, +.reservation-edit-form select:focus { + border-color: var(--blue); +} + +.user-reservation-list { + display: grid; + gap: 14px; + margin-top: 18px; +} + +.user-reservation-card, +.empty-state { + padding: 16px; + border: 1px solid var(--line); + border-radius: 16px; + background: #fbfcff; +} + +.user-reservation-main { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 16px; + align-items: start; +} + +.user-reservation-main h3 { + margin: 10px 0 6px; + font-size: 1.08rem; +} + +.user-reservation-main p, +.empty-state { + margin: 0; + color: var(--muted); + line-height: 1.6; +} + +.reservation-date-time { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + color: var(--muted); +} + +.reservation-date-time strong { + color: var(--text); +} + +.reservation-card-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 14px; +} + +.danger-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 46px; + padding: 0 18px; + border-radius: 12px; + border: 1px solid #fecaca; + background: #fff; + color: #dc2626; + font-size: 0.96rem; + font-weight: 500; + cursor: pointer; +} + +.reservation-edit-form { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--line); +} + +.reservation-edit-form label { + display: flex; + flex-direction: column; + gap: 8px; + color: var(--muted); + font-size: 0.9rem; +} + +.reservation-edit-form .reservation-card-actions { + grid-column: 1 / -1; +} + .full-width { width: 100%; } @@ -492,7 +618,22 @@ input { .primary-button, .secondary-button, - .secondary-link { + .secondary-link, + .danger-button { width: 100%; } + + .lookup-controls, + .user-reservation-main, + .reservation-edit-form { + grid-template-columns: 1fr; + } + + .reservation-date-time { + align-items: flex-start; + } + + .reservation-card-actions { + flex-direction: column; + } } diff --git a/src/main/resources/static/js/times.js b/src/main/resources/static/js/times.js index 9259fb1706..fcea075321 100644 --- a/src/main/resources/static/js/times.js +++ b/src/main/resources/static/js/times.js @@ -5,7 +5,12 @@ document.addEventListener("DOMContentLoaded", () => { rankedThemes: [], times: [], selectedThemeId: null, - selectedDateId: null + selectedDateId: null, + userReservationName: "", + userReservations: [], + editingReservationId: null, + editDateByReservationId: {}, + editTimesByReservationId: {} }; const modalTriggers = document.querySelectorAll("[data-modal-open]"); @@ -22,6 +27,19 @@ document.addEventListener("DOMContentLoaded", () => { const showTimesButton = document.getElementById("show-times-button"); const resetSelectionButton = document.getElementById("reset-selection-button"); const statusStrip = document.getElementById("status-strip"); + const userReservationSearchForm = document.getElementById("user-reservation-search-form"); + const userReservationNameInput = document.getElementById("user-reservation-name"); + const userReservationMessage = document.getElementById("user-reservation-message"); + const userReservationList = document.getElementById("user-reservation-list"); + + document.querySelectorAll("[data-scroll-target]").forEach((button) => { + button.addEventListener("click", () => { + document.getElementById(button.dataset.scrollTarget)?.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + }); + }); modalTriggers.forEach((trigger) => { trigger.addEventListener("click", () => { @@ -67,6 +85,15 @@ document.addEventListener("DOMContentLoaded", () => { return date.slice(5).replace("-", "."); } + function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + } + function findTheme() { return state.themes.find((theme) => theme.id === state.selectedThemeId); } @@ -133,6 +160,154 @@ document.addEventListener("DOMContentLoaded", () => { `).join(""); } + function renderUserReservations() { + if (!state.userReservationName) { + userReservationList.innerHTML = ""; + return; + } + + if (state.userReservations.length === 0) { + userReservationList.innerHTML = ` +
+ ${escapeHtml(state.userReservationName)}님의 예약이 없습니다. +
+ `; + return; + } + + userReservationList.innerHTML = state.userReservations.map((reservation) => { + const isEditing = state.editingReservationId === reservation.reservationId; + const selectedStartWhen = state.editDateByReservationId[reservation.reservationId] || reservation.date.startWhen; + const editTimes = state.editTimesByReservationId[reservation.reservationId] || []; + const dateOptions = state.dates.map((date) => ` + + `).join(""); + const timeOptions = buildEditTimeOptions(reservation, editTimes); + + return ` +
+
+
+ 예약 번호 ${reservation.reservationId} +

${escapeHtml(reservation.theme.name)}

+

${escapeHtml(reservation.theme.content)}

+
+
+ ${reservation.date.startWhen} + ${reservation.time.startAt} +
+
+ ${isEditing ? ` +
+ + +
+ + +
+
+ ` : ` +
+ + +
+ `} +
+ `; + }).join(""); + + bindUserReservationEvents(); + } + + function buildEditTimeOptions(reservation, times) { + const options = new Map(); + options.set(reservation.time.startAt, reservation.time.startAt); + times + .filter((time) => time.available || time.startAt === reservation.time.startAt) + .forEach((time) => options.set(time.startAt, time.startAt)); + + return [...options.values()].map((startAt) => ` + + `).join(""); + } + + function bindUserReservationEvents() { + userReservationList.querySelectorAll("[data-cancel-id]").forEach((button) => { + button.addEventListener("click", async () => { + await cancelReservation(Number(button.dataset.cancelId)); + }); + }); + + userReservationList.querySelectorAll("[data-edit-id]").forEach((button) => { + button.addEventListener("click", async () => { + const reservation = findUserReservation(Number(button.dataset.editId)); + state.editingReservationId = reservation.reservationId; + state.editDateByReservationId[reservation.reservationId] = reservation.date.startWhen; + renderUserReservations(); + await loadEditTimes(reservation, reservation.date.id); + }); + }); + + userReservationList.querySelectorAll("[data-edit-cancel]").forEach((button) => { + button.addEventListener("click", () => { + state.editingReservationId = null; + renderUserReservations(); + }); + }); + + userReservationList.querySelectorAll("[data-edit-date]").forEach((select) => { + select.addEventListener("change", async () => { + const reservation = findUserReservation(Number(select.dataset.id)); + const selectedOption = select.options[select.selectedIndex]; + state.editDateByReservationId[reservation.reservationId] = selectedOption.value; + await loadEditTimes(reservation, Number(selectedOption.dataset.dateId)); + }); + }); + + userReservationList.querySelectorAll("[data-edit-form]").forEach((form) => { + form.addEventListener("submit", async (event) => { + event.preventDefault(); + const formData = new FormData(form); + await updateReservation(Number(form.dataset.id), { + startWhen: formData.get("startWhen"), + startAt: formData.get("startAt") + }); + }); + }); + } + + function findUserReservation(id) { + return state.userReservations.find((reservation) => reservation.reservationId === id); + } + + function setUserReservationMessage(text, type = "") { + userReservationMessage.textContent = text; + userReservationMessage.className = "form-message"; + if (type) { + userReservationMessage.classList.add(type); + } + } + + async function parseResponse(response) { + const text = await response.text(); + if (!text) { + return null; + } + return JSON.parse(text); + } + async function fetchJson(url) { const response = await fetch(url, { headers: { Accept: "application/json" } }); if (!response.ok) { @@ -141,6 +316,74 @@ document.addEventListener("DOMContentLoaded", () => { return response.json(); } + async function loadUserReservations(name) { + const response = await fetch(`/reservations?name=${encodeURIComponent(name)}`, { + headers: { Accept: "application/json" } + }); + const result = await parseResponse(response); + + if (!response.ok) { + throw new Error(result?.message || "예약 목록을 불러오지 못했습니다."); + } + + state.userReservationName = result.name; + state.userReservations = result.reservation; + state.editingReservationId = null; + state.editDateByReservationId = {}; + state.editTimesByReservationId = {}; + renderUserReservations(); + } + + async function cancelReservation(id) { + if (!window.confirm("예약을 취소하시겠습니까?")) { + return; + } + + const response = await fetch(`/reservations/${id}`, { + method: "DELETE", + headers: { Accept: "application/json" } + }); + const result = await parseResponse(response); + + if (!response.ok) { + setUserReservationMessage(result?.message || "예약 취소 중 문제가 발생했습니다.", "error"); + return; + } + + setUserReservationMessage("예약이 취소되었습니다.", "success"); + await loadUserReservations(state.userReservationName); + } + + async function updateReservation(id, payload) { + const response = await fetch(`/reservations/${id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + }, + body: JSON.stringify(payload) + }); + const result = await parseResponse(response); + + if (!response.ok) { + setUserReservationMessage(result?.message || "예약 변경 중 문제가 발생했습니다.", "error"); + return; + } + + setUserReservationMessage("예약이 변경되었습니다.", "success"); + await loadUserReservations(state.userReservationName); + } + + async function loadEditTimes(reservation, dateId) { + try { + const times = await fetchJson(`/times?themeId=${reservation.theme.id}&dateId=${dateId}`); + state.editTimesByReservationId[reservation.reservationId] = times; + renderUserReservations(); + } catch (error) { + setUserReservationMessage("변경 가능한 시간을 불러오지 못했습니다.", "error"); + } + } + async function loadInitialData() { const [themes, dates, rankedThemes] = await Promise.all([ fetchJson("/themes"), @@ -225,6 +468,9 @@ document.addEventListener("DOMContentLoaded", () => { message.textContent = result.name + "님의 예약이 완료되었습니다."; message.classList.add("success"); + userReservationNameInput.value = result.name; + await loadUserReservations(result.name); + window.setTimeout(() => { window.location.reload(); }, 600); @@ -234,6 +480,22 @@ document.addEventListener("DOMContentLoaded", () => { } }); + userReservationSearchForm.addEventListener("submit", async (event) => { + event.preventDefault(); + const name = new FormData(userReservationSearchForm).get("name").trim(); + if (!name) { + setUserReservationMessage("예약자 이름을 입력해 주세요.", "error"); + return; + } + + try { + setUserReservationMessage(""); + await loadUserReservations(name); + } catch (error) { + setUserReservationMessage(error.message, "error"); + } + }); + loadInitialData().catch(() => { updateStatus("초기 데이터를 불러오지 못했습니다."); }); diff --git a/src/main/resources/templates/times.html b/src/main/resources/templates/times.html index df11ce4801..30b547a45a 100644 --- a/src/main/resources/templates/times.html +++ b/src/main/resources/templates/times.html @@ -15,6 +15,7 @@ ROOMESCAPE @@ -91,6 +92,26 @@

예약 정보 입력

+
+
+
+ MY RESERVATIONS +

내 예약 조회

+

예약자 이름으로 예약 목록을 확인하고, 취소하거나 날짜와 시간을 변경할 수 있습니다.

+
+
+
+ +
+ + +
+

+
+
+
+ From 58a4e858170a65be95d15b702b8243dbe77fffd5 Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 14 May 2026 12:23:30 +0900 Subject: [PATCH 11/43] =?UTF-8?q?refactor:=20=EB=8D=94=EB=AF=B8=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/data.sql | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 8a1f779a14..b91d33d4c3 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -6,15 +6,17 @@ VALUES ('10:00'), ('16:00'); INSERT INTO reservation_date (date) -VALUES ('2026-05-01'), - ('2026-05-02'), - ('2026-05-03'), - ('2026-05-04'), - ('2026-05-05'), - ('2026-05-06'), - ('2026-05-07'), - ('2026-05-08'), - ('2026-05-09'); +VALUES ('2026-05-14'), + ('2026-05-15'), + ('2026-05-16'), + ('2026-06-01'), + ('2026-06-02'), + ('2026-06-03'), + ('2026-06-04'), + ('2026-06-05'), + ('2026-06-10'), + ('2026-06-11'), + ('2026-06-12'); INSERT INTO theme (name, content, url) VALUES ('공포', '오금이 저리는 공포입니다.', '/themes/scary'), From ea28c0e68b33164a91e36cbfb99716ffb3777121 Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 14 May 2026 12:24:05 +0900 Subject: [PATCH 12/43] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 534381a284..2764865bc9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,13 @@ - 사용자는 이름, 날짜 ID, 시간 ID, 테마 ID를 입력해 예약을 생성한다. - 예약 생성 시 이름이 비어 있으면 예외를 반환한다. - 예약 생성 시 날짜, 시간, 테마 값이 누락되면 예외를 반환한다. +- 사용자는 이름으로 본인의 예약 목록을 조회한다. +- 사용자는 본인의 예약을 취소한다. +- 사용자는 본인의 예약 날짜와 시간을 변경한다. +- 예약 변경 시 날짜와 시간이 모두 누락되면 예외를 반환한다. +- 예약 변경 시 존재하지 않는 날짜나 시간이 입력되면 예외를 반환한다. +- 예약 변경 시 변경하려는 날짜와 시간이 현재보다 이전이면 예외를 반환한다. +- 예약 변경 시 같은 날짜, 시간, 테마의 다른 예약이 이미 존재하면 예외를 반환한다. - 관리자는 전체 예약 목록을 조회한다. - 관리자는 예약 ID로 예약을 삭제한다. @@ -101,7 +108,7 @@ ```json { - "name": "쿠키", + "name": "보예", "dateId": 1, "timeId": 2, "themeId": 3 @@ -113,7 +120,7 @@ ```json { "id": 29, - "name": "쿠키", + "name": "보예", "date": "2026-05-01", "time": "11:00", "theme": { @@ -124,6 +131,55 @@ } ``` +#### `GET /reservations?name={name}` + +- 설명: 사용자 이름으로 예약 목록 조회 +- 응답 `200 OK` + +```json +{ + "name": "보예", + "reservation": [ + { + "reservationId": 29, + "date": { + "id": 1, + "startWhen": "2026-05-01" + }, + "time": { + "id": 2, + "startAt": "11:00" + }, + "theme": { + "id": 3, + "name": "청춘물", + "content": "학교 배경인 테마 입니다.", + "url": "/themes/youth" + } + } + ] +} +``` + +#### `PATCH /reservations/{id}` + +- 설명: 사용자 예약 날짜와 시간 변경 +- 요청 본문 + +```json +{ + "startWhen": "2026-05-10", + "startAt": "15:00" +} +``` + +- 응답 `204 No Content` + +#### `DELETE /reservations/{id}` + +- 설명: 사용자 예약 취소 +- 응답 `204 No Content` + #### `DELETE /admin/reservations/{id}` - 설명: 예약 삭제 From f7f353531aee8ac86e0e16eaeb341803fda238db Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 14 May 2026 13:20:42 +0900 Subject: [PATCH 13/43] =?UTF-8?q?refactor:=20`@Valid`=EB=A1=9C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B0=92=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../reservation/ReservationController.java | 6 +- .../dto/CreateReservationRequest.java | 30 +++---- .../ReservationDateController.java | 5 +- .../dto/CreateReservationDateRequest.java | 2 + .../ReservationTimeController.java | 6 +- .../dto/CreateTimeRequest.java | 10 +-- .../domain/theme/ThemeController.java | 9 +- .../domain/theme/dto/CreateThemeRequest.java | 9 ++ .../support/exception/ErrorResponse.java | 5 ++ .../exception/GlobalExceptionHandler.java | 42 +++++++-- .../exception/RoomescapeErrorCode.java | 5 +- .../dto/CreateReservationRequestTest.java | 89 ------------------- .../exception/GlobalExceptionHandlerTest.java | 2 +- 14 files changed, 83 insertions(+), 138 deletions(-) delete mode 100644 src/test/java/roomescape/domain/reservation/dto/CreateReservationRequestTest.java diff --git a/build.gradle b/build.gradle index 8c8d6b967c..b041a401f7 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index fd8b41fb3b..76cf1f1bcf 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -1,5 +1,6 @@ package roomescape.domain.reservation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -23,8 +24,9 @@ public class ReservationController { private final ReservationService reservationService; @PostMapping("/reservations") - public ResponseEntity createReservation(@RequestBody CreateReservationRequest request) { - request.validate(); + public ResponseEntity createReservation( + @Valid @RequestBody CreateReservationRequest request + ) { CreateReservationResponse response = reservationService.createReservation(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } diff --git a/src/main/java/roomescape/domain/reservation/dto/CreateReservationRequest.java b/src/main/java/roomescape/domain/reservation/dto/CreateReservationRequest.java index 0306497e49..5ac0166649 100644 --- a/src/main/java/roomescape/domain/reservation/dto/CreateReservationRequest.java +++ b/src/main/java/roomescape/domain/reservation/dto/CreateReservationRequest.java @@ -1,36 +1,28 @@ package roomescape.domain.reservation.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; -import roomescape.support.exception.BadRequestException; -import roomescape.support.exception.ReservationErrorCode; -import roomescape.support.exception.ReservationTimeErrorCode; -import roomescape.support.exception.ThemeErrorCode; public record CreateReservationRequest( + @Size(max = 10, message = "이름은 10자 이하여야 합니다.") + @NotBlank(message = "이름은 비어있을 수 없습니다. 10자 이내의 이름을 입력해주세요.") String name, + + @NotNull(message = "날짜는 필수 선택 사항 입니다. 날짜를 선택해주세요.") Long dateId, + + @NotNull(message = "시간은 필수 선택 사항 입니다. 시간을 선택해주세요.") Long timeId, + + @NotNull(message = "테마는 필수 선택 사항 입니다. 테마를 선택해주세요.") Long themeId ) { - public void validate() { - if (name == null || name.isBlank()) { - throw new BadRequestException(ReservationErrorCode.INVALID_RESERVATION_NAME); - } - if (dateId == null) { - throw new BadRequestException(ReservationErrorCode.INVALID_RESERVATION_DATE); - } - if (timeId == null) { - throw new BadRequestException(ReservationTimeErrorCode.INVALID_RESERVATION_TIME); - } - if (themeId == null) { - throw new BadRequestException(ThemeErrorCode.INVALID_THEME); - } - } - public Reservation toEntity(ReservationDate reservationDate, ReservationTime reservationTime, Theme theme) { return Reservation.createWithoutId( name, diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateController.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateController.java index d4272726bf..ae287659f6 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateController.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateController.java @@ -1,6 +1,7 @@ package roomescape.domain.reservationdate; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -11,11 +12,11 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.support.auth.AdminRequestValidator; import roomescape.domain.reservationdate.dto.AdminReservationDateResponse; import roomescape.domain.reservationdate.dto.CreateReservationDateRequest; import roomescape.domain.reservationdate.dto.CreateReservationDateResponse; import roomescape.domain.reservationdate.dto.ReservationDateResponse; +import roomescape.support.auth.AdminRequestValidator; @RestController @RequiredArgsConstructor @@ -38,7 +39,7 @@ public ResponseEntity> getAllReservationDateF @PostMapping("/admin/reservation-dates") public ResponseEntity createReservationDate( HttpServletRequest httpServletRequest, - @RequestBody CreateReservationDateRequest createReservationDateRequest + @Valid @RequestBody CreateReservationDateRequest createReservationDateRequest ) { if (validator.isUnauthorized(httpServletRequest)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); diff --git a/src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateRequest.java b/src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateRequest.java index db3903782d..666dce39c5 100644 --- a/src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateRequest.java +++ b/src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateRequest.java @@ -1,9 +1,11 @@ package roomescape.domain.reservationdate.dto; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import roomescape.domain.reservationdate.ReservationDate; public record CreateReservationDateRequest( + @NotNull(message = "예약 날짜는 필수 사항 입니다. 날짜를 선택해주세요.") LocalDate reservationDate ) { diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java index 8b69e121d5..211c6ca617 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java @@ -1,6 +1,7 @@ package roomescape.domain.reservationtime; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -12,11 +13,11 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import roomescape.support.auth.AdminRequestValidator; import roomescape.domain.reservationtime.dto.CreateTimeRequest; import roomescape.domain.reservationtime.dto.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; import roomescape.domain.reservationtime.dto.ReservationTimeResponse; +import roomescape.support.auth.AdminRequestValidator; @RestController @RequiredArgsConstructor @@ -37,9 +38,8 @@ public ResponseEntity> getAllReservationTime(HttpS @PostMapping("/admin/times") public ResponseEntity createReservationTime( HttpServletRequest httpServletRequest, - @RequestBody CreateTimeRequest createTimeRequest + @Valid @RequestBody CreateTimeRequest createTimeRequest ) { - createTimeRequest.validate(); if (validator.isUnauthorized(httpServletRequest)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } diff --git a/src/main/java/roomescape/domain/reservationtime/dto/CreateTimeRequest.java b/src/main/java/roomescape/domain/reservationtime/dto/CreateTimeRequest.java index c06202d203..04384fe625 100644 --- a/src/main/java/roomescape/domain/reservationtime/dto/CreateTimeRequest.java +++ b/src/main/java/roomescape/domain/reservationtime/dto/CreateTimeRequest.java @@ -1,20 +1,14 @@ package roomescape.domain.reservationtime.dto; +import jakarta.validation.constraints.NotNull; import java.time.LocalTime; import roomescape.domain.reservationtime.ReservationTime; -import roomescape.support.exception.BadRequestException; -import roomescape.support.exception.ReservationTimeErrorCode; public record CreateTimeRequest( + @NotNull(message = "시간은 필수 사항 입니다. 시간을 선택해주세요.") LocalTime startAt ) { - public void validate() { - if (startAt == null) { - throw new BadRequestException(ReservationTimeErrorCode.INVALID_RESERVATION_TIME); - } - } - public ReservationTime toEntity() { return ReservationTime.createWithoutId(startAt); } diff --git a/src/main/java/roomescape/domain/theme/ThemeController.java b/src/main/java/roomescape/domain/theme/ThemeController.java index 33183c6496..19a22017a3 100644 --- a/src/main/java/roomescape/domain/theme/ThemeController.java +++ b/src/main/java/roomescape/domain/theme/ThemeController.java @@ -1,6 +1,7 @@ package roomescape.domain.theme; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -11,12 +12,12 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.support.auth.AdminRequestValidator; import roomescape.domain.theme.dto.AdminThemeResponse; import roomescape.domain.theme.dto.CreateThemeRequest; import roomescape.domain.theme.dto.CreateThemeResponse; import roomescape.domain.theme.dto.ThemeRankResponse; import roomescape.domain.theme.dto.ThemeResponse; +import roomescape.support.auth.AdminRequestValidator; @RestController @RequiredArgsConstructor @@ -35,8 +36,10 @@ public ResponseEntity> getAllThemeForAdmin(HttpServletR } @PostMapping("/admin/themes") - public ResponseEntity createTheme(@RequestBody CreateThemeRequest createThemeRequest, - HttpServletRequest httpServletRequest) { + public ResponseEntity createTheme( + @Valid @RequestBody CreateThemeRequest createThemeRequest, + HttpServletRequest httpServletRequest + ) { if (validator.isUnauthorized(httpServletRequest)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } diff --git a/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java b/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java index b9c1ffb3e0..9f092227d8 100644 --- a/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java +++ b/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java @@ -1,10 +1,19 @@ package roomescape.domain.theme.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import roomescape.domain.theme.Theme; public record CreateThemeRequest( + @Size(max = 10, message = "이름은 10자 이하여야 합니다. 다시 입력해주세요.") + @NotBlank(message = "이름은 비어있을 수 없습니다. 10자 이하의 이름을 입력해주세요.") String name, + + @NotNull(message = "테마 내용은 필수 입력값 입니다. 테마에 대한 설명을 입력해주세요.") String content, + + @NotNull(message = "url은 비어있을 수 없습니다. url을 입력해주세요.") String url ) { diff --git a/src/main/java/roomescape/support/exception/ErrorResponse.java b/src/main/java/roomescape/support/exception/ErrorResponse.java index 5776da2b87..3a5fc31eeb 100644 --- a/src/main/java/roomescape/support/exception/ErrorResponse.java +++ b/src/main/java/roomescape/support/exception/ErrorResponse.java @@ -17,4 +17,9 @@ public static ResponseEntity of(HttpStatus httpStatus, ErrorCode return ResponseEntity.status(httpStatus) .body(new ErrorResponse(errorCode.getCode(), errorCode.getMessage())); } + + public static ResponseEntity of(HttpStatus httpStatus, ErrorCode errorCode, String message) { + return ResponseEntity.status(httpStatus) + .body(new ErrorResponse(errorCode.getCode(), message)); + } } diff --git a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java index b863105443..4c8b1d7272 100644 --- a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java @@ -2,6 +2,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -9,27 +12,48 @@ public class GlobalExceptionHandler { @ExceptionHandler(BadRequestException.class) - public ResponseEntity handleBadRequestException(BadRequestException exception) { - return ErrorResponse.of(HttpStatus.BAD_REQUEST, exception); + public ResponseEntity handleBadRequestException(BadRequestException e) { + return ErrorResponse.of(HttpStatus.BAD_REQUEST, e); } @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException exception) { - return ErrorResponse.of(HttpStatus.NOT_FOUND, exception); + public ResponseEntity handleNotFoundException(NotFoundException e) { + return ErrorResponse.of(HttpStatus.NOT_FOUND, e); } @ExceptionHandler(ConflictException.class) - public ResponseEntity handleConflictException(ConflictException exception) { - return ErrorResponse.of(HttpStatus.CONFLICT, exception); + public ResponseEntity handleConflictException(ConflictException e) { + return ErrorResponse.of(HttpStatus.CONFLICT, e); } @ExceptionHandler(InternalServerException.class) - public ResponseEntity handleInternalServerException(InternalServerException exception) { - return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, exception); + public ResponseEntity handleInternalServerException(InternalServerException e) { + return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, e); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException e + ) { + + String message = e.getAllErrors() + .stream() + .findFirst() + .map(ObjectError::getDefaultMessage) + .orElse(RoomescapeErrorCode.INPUT_VALIDATION_ERROR.getMessage()); + + return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrorCode.INPUT_VALIDATION_ERROR, message); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException( + HttpMessageNotReadableException e + ) { + return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrorCode.INPUT_FORMAT_ERROR); } @ExceptionHandler(Exception.class) - public ResponseEntity handleException(Exception exception) { + public ResponseEntity handleException(Exception e) { return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, RoomescapeErrorCode.INTERNAL_SERVER_ERROR); } } diff --git a/src/main/java/roomescape/support/exception/RoomescapeErrorCode.java b/src/main/java/roomescape/support/exception/RoomescapeErrorCode.java index cec2288ea0..60db04a821 100644 --- a/src/main/java/roomescape/support/exception/RoomescapeErrorCode.java +++ b/src/main/java/roomescape/support/exception/RoomescapeErrorCode.java @@ -5,8 +5,9 @@ @Getter public enum RoomescapeErrorCode implements ErrorCode { - BAD_REQUEST("잘못된 요청입니다."), - INTERNAL_SERVER_ERROR("서버 내부 오류가 발생했습니다"), + INPUT_FORMAT_ERROR("입력 형식이 올바르지 않습니다. 날짜는 yyyy-MM-dd, 시간은 HH:mm 형식으로 입력해주세요."), + INPUT_VALIDATION_ERROR("입력 검증 오류가 발생했습니다."), + INTERNAL_SERVER_ERROR("서버 내부 오류가 발생했습니다."), INVALID_GENERATED_KEY("생성 키를 조회할 수 없습니다."), ; diff --git a/src/test/java/roomescape/domain/reservation/dto/CreateReservationRequestTest.java b/src/test/java/roomescape/domain/reservation/dto/CreateReservationRequestTest.java deleted file mode 100644 index fdf4749b44..0000000000 --- a/src/test/java/roomescape/domain/reservation/dto/CreateReservationRequestTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package roomescape.domain.reservation.dto; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.junit.jupiter.api.Test; -import roomescape.support.exception.RoomescapeException; - -class CreateReservationRequestTest { - - @Test - void 이름이_null이면_예외가_발생한다() { - // given - CreateReservationRequest request = new CreateReservationRequest( - null, - 1L, - 1L, - 1L - ); - - // when & then - assertThatThrownBy(request::validate) - .isInstanceOf(RoomescapeException.class) - .hasMessage("이름은 비어 있을 수 없습니다."); - } - - @Test - void 이름이_공백이면_예외가_발생한다() { - // given - CreateReservationRequest request = new CreateReservationRequest( - " ", - 1L, - 1L, - 1L - ); - - // when & then - assertThatThrownBy(request::validate) - .isInstanceOf(RoomescapeException.class) - .hasMessage("이름은 비어 있을 수 없습니다."); - } - - @Test - void 날짜가_null이면_예외가_발생한다() { - // given - CreateReservationRequest request = new CreateReservationRequest( - "보예", - null, - 1L, - 1L - ); - - // when & then - assertThatThrownBy(request::validate) - .isInstanceOf(RoomescapeException.class) - .hasMessage("날짜는 필수입니다."); - } - - @Test - void 시간_id가_null이면_예외가_발생한다() { - // given - CreateReservationRequest request = new CreateReservationRequest( - "보예", - 1L, - null, - 1L - ); - - // when & then - assertThatThrownBy(request::validate) - .isInstanceOf(RoomescapeException.class) - .hasMessage("시간은 필수입니다."); - } - - @Test - void 테마_id가_null이면_예외가_발생한다() { - // given - CreateReservationRequest request = new CreateReservationRequest( - "보예", - 1L, - 1L, - null - ); - - // when & then - assertThatThrownBy(request::validate) - .isInstanceOf(RoomescapeException.class) - .hasMessage("테마는 필수입니다."); - } -} diff --git a/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java b/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java index 51455827dd..8b9a46312d 100644 --- a/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java +++ b/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java @@ -37,7 +37,7 @@ class GlobalExceptionHandlerTest { assertSoftly(softly -> { softly.assertThat(response.getStatusCode().value()).isEqualTo(500); softly.assertThat(response.getBody().code()).isEqualTo("INTERNAL_SERVER_ERROR"); - softly.assertThat(response.getBody().message()).isEqualTo("서버 내부 오류가 발생했습니다"); + softly.assertThat(response.getBody().message()).isEqualTo("서버 내부 오류가 발생했습니다."); }); } } From 30bbe9c82289f6f2b43719eb18da62dd9c43b6b5 Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 14 May 2026 13:42:58 +0900 Subject: [PATCH 14/43] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B0=9C=ED=96=89=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/support/exception/GlobalExceptionHandler.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java index 4c8b1d7272..ef9ca9dc66 100644 --- a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java @@ -32,9 +32,7 @@ public ResponseEntity handleInternalServerException(InternalServe } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValidException( - MethodArgumentNotValidException e - ) { + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { String message = e.getAllErrors() .stream() From ca09e57488065c253b1deb0ee0d188bcdc69f5a8 Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 14 May 2026 16:57:30 +0900 Subject: [PATCH 15/43] =?UTF-8?q?refactor:=20ErrorCode=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/Reservation.java | 14 ++++---- .../reservation/ReservationService.java | 36 +++++++++---------- .../JdbcReservationDateRepository.java | 4 +-- .../ReservationDateService.java | 4 +-- .../JdbcReservationTimeRepository.java | 4 +-- .../reservationtime/ReservationTime.java | 4 +-- .../ReservationTimeService.java | 4 +-- .../domain/theme/JdbcThemeRepository.java | 4 +-- .../roomescape/domain/theme/ThemeService.java | 4 +-- .../exception/BadRequestException.java | 4 +-- .../support/exception/ConflictException.java | 4 +-- .../support/exception/ErrorResponse.java | 10 +++--- .../exception/{ErrorCode.java => Errors.java} | 2 +- .../exception/GlobalExceptionHandler.java | 8 ++--- .../exception/InternalServerException.java | 4 +-- .../support/exception/NotFoundException.java | 4 +-- ...orCode.java => ReservationDateErrors.java} | 4 +-- ...nErrorCode.java => ReservationErrors.java} | 4 +-- ...orCode.java => ReservationTimeErrors.java} | 4 +-- ...peErrorCode.java => RoomescapeErrors.java} | 4 +-- .../exception/RoomescapeException.java | 8 ++--- .../{ThemeErrorCode.java => ThemeErrors.java} | 4 +-- .../exception/GlobalExceptionHandlerTest.java | 2 +- 23 files changed, 72 insertions(+), 72 deletions(-) rename src/main/java/roomescape/support/exception/{ErrorCode.java => Errors.java} (75%) rename src/main/java/roomescape/support/exception/{ReservationDateErrorCode.java => ReservationDateErrors.java} (85%) rename src/main/java/roomescape/support/exception/{ReservationErrorCode.java => ReservationErrors.java} (86%) rename src/main/java/roomescape/support/exception/{ReservationTimeErrorCode.java => ReservationTimeErrors.java} (88%) rename src/main/java/roomescape/support/exception/{RoomescapeErrorCode.java => RoomescapeErrors.java} (85%) rename src/main/java/roomescape/support/exception/{ThemeErrorCode.java => ThemeErrors.java} (83%) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index d5ddaf6e34..fc9f5e2f2f 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -5,9 +5,9 @@ import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; import roomescape.support.exception.BadRequestException; -import roomescape.support.exception.ReservationErrorCode; -import roomescape.support.exception.ReservationTimeErrorCode; -import roomescape.support.exception.ThemeErrorCode; +import roomescape.support.exception.ReservationErrors; +import roomescape.support.exception.ReservationTimeErrors; +import roomescape.support.exception.ThemeErrors; @Getter public class Reservation { @@ -76,16 +76,16 @@ public static Reservation of( private static void validate(String name, ReservationDate date, ReservationTime time, Theme theme) { if (name == null || name.isBlank()) { - throw new BadRequestException(ReservationErrorCode.INVALID_RESERVATION_NAME); + throw new BadRequestException(ReservationErrors.INVALID_RESERVATION_NAME); } if (date == null) { - throw new BadRequestException(ReservationErrorCode.INVALID_RESERVATION_DATE); + throw new BadRequestException(ReservationErrors.INVALID_RESERVATION_DATE); } if (time == null) { - throw new BadRequestException(ReservationTimeErrorCode.INVALID_RESERVATION_TIME); + throw new BadRequestException(ReservationTimeErrors.INVALID_RESERVATION_TIME); } if (theme == null) { - throw new BadRequestException(ThemeErrorCode.INVALID_THEME); + throw new BadRequestException(ThemeErrors.INVALID_THEME); } } } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 5e0a4c6c3a..5e4ca78b45 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -21,10 +21,10 @@ import roomescape.domain.theme.ThemeRepository; import roomescape.support.exception.BadRequestException; import roomescape.support.exception.NotFoundException; -import roomescape.support.exception.ReservationDateErrorCode; -import roomescape.support.exception.ReservationErrorCode; -import roomescape.support.exception.ReservationTimeErrorCode; -import roomescape.support.exception.ThemeErrorCode; +import roomescape.support.exception.ReservationDateErrors; +import roomescape.support.exception.ReservationErrors; +import roomescape.support.exception.ReservationTimeErrors; +import roomescape.support.exception.ThemeErrors; @Slf4j @Service @@ -41,12 +41,12 @@ public class ReservationService { @Transactional public CreateReservationResponse createReservation(CreateReservationRequest request) { ReservationTime reservationTime = reservationTimeRepository.findById(request.timeId()) - .orElseThrow(() -> new NotFoundException(ReservationTimeErrorCode.RESERVATION_TIME_NOT_EXIST)); + .orElseThrow(() -> new NotFoundException(ReservationTimeErrors.RESERVATION_TIME_NOT_EXIST)); ReservationDate reservationDate = reservationDateRepository.findById(request.dateId()) - .orElseThrow(() -> new NotFoundException(ReservationDateErrorCode.RESERVATION_DATE_NOT_EXIST)); + .orElseThrow(() -> new NotFoundException(ReservationDateErrors.RESERVATION_DATE_NOT_EXIST)); validateReservationScheduleToCreate(reservationDate, reservationTime); Theme theme = themeRepository.findById(request.themeId()) - .orElseThrow(() -> new NotFoundException(ThemeErrorCode.THEME_NOT_EXIST)); + .orElseThrow(() -> new NotFoundException(ThemeErrors.THEME_NOT_EXIST)); validateDuplicated(reservationTime, reservationDate, theme); Reservation savedReservation = reservationRepository.save( request.toEntity(reservationDate, reservationTime, theme)); @@ -75,7 +75,7 @@ public void deleteReservationByAdmin(Long id) { @Transactional public void deleteUserReservation(Long id) { Reservation reservation = reservationRepository.findById(id) - .orElseThrow(() -> new NotFoundException(ReservationErrorCode.RESERVATION_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(ReservationErrors.RESERVATION_NOT_FOUND)); validateUserCanDeleteReservation(reservation); reservationRepository.deleteById(id); } @@ -83,7 +83,7 @@ public void deleteUserReservation(Long id) { @Transactional public void updateReservation(Long id, UpdateReservationRequest request) { Reservation reservation = reservationRepository.findById(id) - .orElseThrow(() -> new NotFoundException(ReservationErrorCode.RESERVATION_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(ReservationErrors.RESERVATION_NOT_FOUND)); ReservationTime reservationTime = reservation.getTime(); ReservationDate reservationDate = reservation.getDate(); reservationTime = getReservationTime(request, reservationTime); @@ -97,7 +97,7 @@ public void updateReservation(Long id, UpdateReservationRequest request) { reservation.getTheme() ); reservationRepository.update(id, updatedReservation) - .orElseThrow(() -> new NotFoundException(ReservationErrorCode.RESERVATION_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(ReservationErrors.RESERVATION_NOT_FOUND)); } private void validateReservationScheduleToCreate( @@ -107,16 +107,16 @@ private void validateReservationScheduleToCreate( LocalDate today = LocalDate.now(clock); LocalTime now = LocalTime.now(clock); if (isPastDate(reservationDate, today)) { - throw new BadRequestException(ReservationDateErrorCode.RESERVATION_DATE_MUST_BE_TODAY_OR_LATER, today); + throw new BadRequestException(ReservationDateErrors.RESERVATION_DATE_MUST_BE_TODAY_OR_LATER, today); } if (isPastTimeToday(reservationDate, reservationTime, today, now)) { - throw new BadRequestException(ReservationTimeErrorCode.RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER, now); + throw new BadRequestException(ReservationTimeErrors.RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER, now); } } private void validateDuplicated(ReservationTime reservationTime, ReservationDate reservationDate, Theme theme) { if (isExistReservation(reservationTime, reservationDate, theme)) { - throw new BadRequestException(ReservationErrorCode.DUPLICATED_RESERVATION); + throw new BadRequestException(ReservationErrors.DUPLICATED_RESERVATION); } } @@ -136,7 +136,7 @@ private void validateDuplicatedWithOther( reservationDate.getId(), theme.getId() )) { - throw new BadRequestException(ReservationErrorCode.DUPLICATED_RESERVATION); + throw new BadRequestException(ReservationErrors.DUPLICATED_RESERVATION); } } @@ -144,10 +144,10 @@ private void validateUserCanDeleteReservation(Reservation reservation) { LocalDate today = LocalDate.now(clock); LocalTime now = LocalTime.now(clock); if (isPastDate(reservation.getDate(), today)) { - throw new BadRequestException(ReservationDateErrorCode.PAST_RESERVATION_DATE_CANNOT_BE_DELETED, today); + throw new BadRequestException(ReservationDateErrors.PAST_RESERVATION_DATE_CANNOT_BE_DELETED, today); } if (isPastTimeToday(reservation.getDate(), reservation.getTime(), today, now)) { - throw new BadRequestException(ReservationTimeErrorCode.PAST_RESERVATION_TiME_CANNOT_BE_DELETED, now); + throw new BadRequestException(ReservationTimeErrors.PAST_RESERVATION_TiME_CANNOT_BE_DELETED, now); } } @@ -167,7 +167,7 @@ private boolean isPastTimeToday( private ReservationDate getReservationDate(UpdateReservationRequest request, ReservationDate reservationDate) { if (request.startWhen() != null) { reservationDate = reservationDateRepository.findByDate(request.startWhen()) - .orElseThrow(() -> new NotFoundException(ReservationDateErrorCode.RESERVATION_DATE_NOT_EXIST)); + .orElseThrow(() -> new NotFoundException(ReservationDateErrors.RESERVATION_DATE_NOT_EXIST)); } return reservationDate; } @@ -175,7 +175,7 @@ private ReservationDate getReservationDate(UpdateReservationRequest request, Res private ReservationTime getReservationTime(UpdateReservationRequest request, ReservationTime reservationTime) { if (request.startAt() != null) { reservationTime = reservationTimeRepository.findByStartAt(request.startAt()) - .orElseThrow(() -> new NotFoundException(ReservationTimeErrorCode.RESERVATION_TIME_NOT_EXIST)); + .orElseThrow(() -> new NotFoundException(ReservationTimeErrors.RESERVATION_TIME_NOT_EXIST)); } return reservationTime; } diff --git a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java index 7133af6352..3ddfd5b932 100644 --- a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java +++ b/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java @@ -13,7 +13,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.RoomescapeErrorCode; +import roomescape.support.exception.RoomescapeErrors; @Repository @RequiredArgsConstructor @@ -76,7 +76,7 @@ private RowMapper reservationDateRowMapper() { private long extractId(KeyHolder keyHolder) { if (keyHolder.getKey() == null) { - throw new InternalServerException(RoomescapeErrorCode.INVALID_GENERATED_KEY); + throw new InternalServerException(RoomescapeErrors.INVALID_GENERATED_KEY); } return keyHolder.getKey().longValue(); } diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java index 3df6e7590f..6b59ab8e04 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java @@ -10,7 +10,7 @@ import roomescape.domain.reservationdate.dto.CreateReservationDateResponse; import roomescape.domain.reservationdate.dto.ReservationDateResponse; import roomescape.support.exception.ConflictException; -import roomescape.support.exception.ReservationDateErrorCode; +import roomescape.support.exception.ReservationDateErrors; @Slf4j @Service @@ -33,7 +33,7 @@ public CreateReservationDateResponse createReservationDate(CreateReservationDate public void deleteReservationDate(Long id) { if (reservationRepository.countByReservationDateId(id) > 0) { - throw new ConflictException(ReservationDateErrorCode.RESERVATION_DATE_IN_USE); + throw new ConflictException(ReservationDateErrors.RESERVATION_DATE_IN_USE); } int deletedCount = reservationDateRepository.deleteById(id); if (deletedCount == 0) { diff --git a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java index 59479a2879..115cd191ec 100644 --- a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java +++ b/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java @@ -13,7 +13,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.RoomescapeErrorCode; +import roomescape.support.exception.RoomescapeErrors; @Repository @RequiredArgsConstructor @@ -76,7 +76,7 @@ private RowMapper reservationTimeRowMapper() { private long extractId(KeyHolder keyHolder) { if (keyHolder.getKey() == null) { - throw new InternalServerException(RoomescapeErrorCode.INVALID_GENERATED_KEY); + throw new InternalServerException(RoomescapeErrors.INVALID_GENERATED_KEY); } 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 bc88ab0a66..bccfa37be9 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java @@ -3,7 +3,7 @@ import java.time.LocalTime; import lombok.Getter; import roomescape.support.exception.BadRequestException; -import roomescape.support.exception.ReservationTimeErrorCode; +import roomescape.support.exception.ReservationTimeErrors; @Getter public class ReservationTime { @@ -33,7 +33,7 @@ public static ReservationTime of(Long id, LocalTime startAt) { private static void validate(LocalTime startAt) { if (startAt == null) { - throw new BadRequestException(ReservationTimeErrorCode.INVALID_RESERVATION_TIME); + throw new BadRequestException(ReservationTimeErrors.INVALID_RESERVATION_TIME); } } diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java index f5590831f1..f8ce3d7375 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java @@ -12,7 +12,7 @@ import roomescape.domain.reservationtime.dto.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; import roomescape.domain.reservationtime.dto.ReservationTimeResponse; -import roomescape.support.exception.ReservationTimeErrorCode; +import roomescape.support.exception.ReservationTimeErrors; @Slf4j @Service @@ -35,7 +35,7 @@ public List getAllReservationTime() { public void deleteReservationTime(Long id) { if (reservationRepository.countByTimeId(id) > 0) { - throw new ConflictException(ReservationTimeErrorCode.RESERVATION_TIME_IN_USE); + throw new ConflictException(ReservationTimeErrors.RESERVATION_TIME_IN_USE); } int deletedCount = reservationTimeRepository.deleteById(id); if (deletedCount == 0) { diff --git a/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java b/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java index 992c2b561d..92fed41546 100644 --- a/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java +++ b/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java @@ -11,7 +11,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.RoomescapeErrorCode; +import roomescape.support.exception.RoomescapeErrors; @Repository @RequiredArgsConstructor @@ -75,7 +75,7 @@ private RowMapper themeRowMapper() { private long extractId(KeyHolder keyHolder) { if (keyHolder.getKey() == null) { - throw new InternalServerException(RoomescapeErrorCode.INVALID_GENERATED_KEY); + throw new InternalServerException(RoomescapeErrors.INVALID_GENERATED_KEY); } return keyHolder.getKey().longValue(); } diff --git a/src/main/java/roomescape/domain/theme/ThemeService.java b/src/main/java/roomescape/domain/theme/ThemeService.java index a36741fe11..68876f2ba9 100644 --- a/src/main/java/roomescape/domain/theme/ThemeService.java +++ b/src/main/java/roomescape/domain/theme/ThemeService.java @@ -12,7 +12,7 @@ import roomescape.domain.theme.dto.ThemeRankResponse; import roomescape.domain.theme.dto.ThemeResponse; import roomescape.support.exception.ConflictException; -import roomescape.support.exception.ThemeErrorCode; +import roomescape.support.exception.ThemeErrors; @Slf4j @Service @@ -38,7 +38,7 @@ public CreateThemeResponse createTheme(CreateThemeRequest request) { public void deleteTheme(Long id) { if (reservationRepository.countByThemeId(id) > 0) { - throw new ConflictException(ThemeErrorCode.THEME_IN_USE); + throw new ConflictException(ThemeErrors.THEME_IN_USE); } int deletedCount = themeRepository.deleteById(id); if (deletedCount == 0) { diff --git a/src/main/java/roomescape/support/exception/BadRequestException.java b/src/main/java/roomescape/support/exception/BadRequestException.java index 0c8e35e7df..d17c80b8f5 100644 --- a/src/main/java/roomescape/support/exception/BadRequestException.java +++ b/src/main/java/roomescape/support/exception/BadRequestException.java @@ -2,7 +2,7 @@ public class BadRequestException extends RoomescapeException { - public BadRequestException(ErrorCode errorCode, Object... args) { - super(errorCode, args); + public BadRequestException(Errors errors, Object... args) { + super(errors, args); } } diff --git a/src/main/java/roomescape/support/exception/ConflictException.java b/src/main/java/roomescape/support/exception/ConflictException.java index 4ebc0e1cd5..5007b4f637 100644 --- a/src/main/java/roomescape/support/exception/ConflictException.java +++ b/src/main/java/roomescape/support/exception/ConflictException.java @@ -2,7 +2,7 @@ public class ConflictException extends RoomescapeException { - public ConflictException(ErrorCode errorCode, Object... args) { - super(errorCode, args); + public ConflictException(Errors errors, Object... args) { + super(errors, args); } } diff --git a/src/main/java/roomescape/support/exception/ErrorResponse.java b/src/main/java/roomescape/support/exception/ErrorResponse.java index 3a5fc31eeb..f5851c6a4f 100644 --- a/src/main/java/roomescape/support/exception/ErrorResponse.java +++ b/src/main/java/roomescape/support/exception/ErrorResponse.java @@ -10,16 +10,16 @@ public record ErrorResponse( public static ResponseEntity of(HttpStatus httpStatus, RoomescapeException exception) { return ResponseEntity.status(httpStatus) - .body(new ErrorResponse(exception.getErrorCode().getCode(), exception.getMessage())); + .body(new ErrorResponse(exception.getErrors().getCode(), exception.getMessage())); } - public static ResponseEntity of(HttpStatus httpStatus, ErrorCode errorCode) { + public static ResponseEntity of(HttpStatus httpStatus, Errors errors) { return ResponseEntity.status(httpStatus) - .body(new ErrorResponse(errorCode.getCode(), errorCode.getMessage())); + .body(new ErrorResponse(errors.getCode(), errors.getMessage())); } - public static ResponseEntity of(HttpStatus httpStatus, ErrorCode errorCode, String message) { + public static ResponseEntity of(HttpStatus httpStatus, Errors errors, String message) { return ResponseEntity.status(httpStatus) - .body(new ErrorResponse(errorCode.getCode(), message)); + .body(new ErrorResponse(errors.getCode(), message)); } } diff --git a/src/main/java/roomescape/support/exception/ErrorCode.java b/src/main/java/roomescape/support/exception/Errors.java similarity index 75% rename from src/main/java/roomescape/support/exception/ErrorCode.java rename to src/main/java/roomescape/support/exception/Errors.java index 6e5dd03b8f..5db80e0fa0 100644 --- a/src/main/java/roomescape/support/exception/ErrorCode.java +++ b/src/main/java/roomescape/support/exception/Errors.java @@ -1,6 +1,6 @@ package roomescape.support.exception; -public interface ErrorCode { +public interface Errors { String getMessage(); diff --git a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java index ef9ca9dc66..b7ef750988 100644 --- a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java @@ -38,20 +38,20 @@ public ResponseEntity handleMethodArgumentNotValidException(Metho .stream() .findFirst() .map(ObjectError::getDefaultMessage) - .orElse(RoomescapeErrorCode.INPUT_VALIDATION_ERROR.getMessage()); + .orElse(RoomescapeErrors.INPUT_VALIDATION_ERROR.getMessage()); - return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrorCode.INPUT_VALIDATION_ERROR, message); + return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrors.INPUT_VALIDATION_ERROR, message); } @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity handleHttpMessageNotReadableException( HttpMessageNotReadableException e ) { - return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrorCode.INPUT_FORMAT_ERROR); + return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrors.INPUT_FORMAT_ERROR); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { - return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, RoomescapeErrorCode.INTERNAL_SERVER_ERROR); + return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, RoomescapeErrors.INTERNAL_SERVER_ERROR); } } diff --git a/src/main/java/roomescape/support/exception/InternalServerException.java b/src/main/java/roomescape/support/exception/InternalServerException.java index 4b16606ff2..2a2a43852a 100644 --- a/src/main/java/roomescape/support/exception/InternalServerException.java +++ b/src/main/java/roomescape/support/exception/InternalServerException.java @@ -2,7 +2,7 @@ public class InternalServerException extends RoomescapeException { - public InternalServerException(ErrorCode errorCode, Object... args) { - super(errorCode, args); + public InternalServerException(Errors errors, Object... args) { + super(errors, args); } } diff --git a/src/main/java/roomescape/support/exception/NotFoundException.java b/src/main/java/roomescape/support/exception/NotFoundException.java index 4e5e7f61b8..aef444a06d 100644 --- a/src/main/java/roomescape/support/exception/NotFoundException.java +++ b/src/main/java/roomescape/support/exception/NotFoundException.java @@ -2,7 +2,7 @@ public class NotFoundException extends RoomescapeException { - public NotFoundException(ErrorCode errorCode, Object... args) { - super(errorCode, args); + public NotFoundException(Errors errors, Object... args) { + super(errors, args); } } diff --git a/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java b/src/main/java/roomescape/support/exception/ReservationDateErrors.java similarity index 85% rename from src/main/java/roomescape/support/exception/ReservationDateErrorCode.java rename to src/main/java/roomescape/support/exception/ReservationDateErrors.java index 170c42ab28..77bf6100d9 100644 --- a/src/main/java/roomescape/support/exception/ReservationDateErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationDateErrors.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public enum ReservationDateErrorCode implements ErrorCode { +public enum ReservationDateErrors implements Errors { RESERVATION_DATE_NOT_EXIST("존재하지 않는 날짜 입니다."), RESERVATION_DATE_IN_USE("이미 예약이 존재하는 날짜는 삭제할 수 없습니다."), @@ -13,7 +13,7 @@ public enum ReservationDateErrorCode implements ErrorCode { private final String message; - ReservationDateErrorCode(String message) { + ReservationDateErrors(String message) { this.message = message; } diff --git a/src/main/java/roomescape/support/exception/ReservationErrorCode.java b/src/main/java/roomescape/support/exception/ReservationErrors.java similarity index 86% rename from src/main/java/roomescape/support/exception/ReservationErrorCode.java rename to src/main/java/roomescape/support/exception/ReservationErrors.java index aed73775bc..4307ec1206 100644 --- a/src/main/java/roomescape/support/exception/ReservationErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationErrors.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public enum ReservationErrorCode implements ErrorCode { +public enum ReservationErrors implements Errors { INVALID_RESERVATION_NAME("이름은 비어 있을 수 없습니다."), INVALID_RESERVATION_DATE("날짜는 필수입니다."), @@ -14,7 +14,7 @@ public enum ReservationErrorCode implements ErrorCode { private final String message; - ReservationErrorCode(String message) { + ReservationErrors(String message) { this.message = message; } diff --git a/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java b/src/main/java/roomescape/support/exception/ReservationTimeErrors.java similarity index 88% rename from src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java rename to src/main/java/roomescape/support/exception/ReservationTimeErrors.java index 4b12751d46..c39a0633c4 100644 --- a/src/main/java/roomescape/support/exception/ReservationTimeErrorCode.java +++ b/src/main/java/roomescape/support/exception/ReservationTimeErrors.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public enum ReservationTimeErrorCode implements ErrorCode { +public enum ReservationTimeErrors implements Errors { INVALID_RESERVATION_TIME("시간은 필수입니다."), INVALID_RESERVATION_TIME_FORMAT("시간은 HH:MM 형식이어야 합니다."), @@ -15,7 +15,7 @@ public enum ReservationTimeErrorCode implements ErrorCode { private final String message; - ReservationTimeErrorCode(String message) { + ReservationTimeErrors(String message) { this.message = message; } diff --git a/src/main/java/roomescape/support/exception/RoomescapeErrorCode.java b/src/main/java/roomescape/support/exception/RoomescapeErrors.java similarity index 85% rename from src/main/java/roomescape/support/exception/RoomescapeErrorCode.java rename to src/main/java/roomescape/support/exception/RoomescapeErrors.java index 60db04a821..beb7ad6e41 100644 --- a/src/main/java/roomescape/support/exception/RoomescapeErrorCode.java +++ b/src/main/java/roomescape/support/exception/RoomescapeErrors.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public enum RoomescapeErrorCode implements ErrorCode { +public enum RoomescapeErrors implements Errors { INPUT_FORMAT_ERROR("입력 형식이 올바르지 않습니다. 날짜는 yyyy-MM-dd, 시간은 HH:mm 형식으로 입력해주세요."), INPUT_VALIDATION_ERROR("입력 검증 오류가 발생했습니다."), @@ -13,7 +13,7 @@ public enum RoomescapeErrorCode implements ErrorCode { private final String message; - RoomescapeErrorCode(String message) { + RoomescapeErrors(String message) { this.message = message; } diff --git a/src/main/java/roomescape/support/exception/RoomescapeException.java b/src/main/java/roomescape/support/exception/RoomescapeException.java index b1a22e21e5..d9d819befd 100644 --- a/src/main/java/roomescape/support/exception/RoomescapeException.java +++ b/src/main/java/roomescape/support/exception/RoomescapeException.java @@ -5,10 +5,10 @@ @Getter public abstract class RoomescapeException extends RuntimeException { - private final ErrorCode errorCode; + private final Errors errors; - public RoomescapeException(ErrorCode errorCode, Object... args) { - super(String.format(errorCode.getMessage(), args)); - this.errorCode = errorCode; + public RoomescapeException(Errors errors, Object... args) { + super(String.format(errors.getMessage(), args)); + this.errors = errors; } } diff --git a/src/main/java/roomescape/support/exception/ThemeErrorCode.java b/src/main/java/roomescape/support/exception/ThemeErrors.java similarity index 83% rename from src/main/java/roomescape/support/exception/ThemeErrorCode.java rename to src/main/java/roomescape/support/exception/ThemeErrors.java index df48ff6a5a..373361f37e 100644 --- a/src/main/java/roomescape/support/exception/ThemeErrorCode.java +++ b/src/main/java/roomescape/support/exception/ThemeErrors.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public enum ThemeErrorCode implements ErrorCode { +public enum ThemeErrors implements Errors { INVALID_THEME("테마는 필수입니다."), THEME_NOT_EXIST("존재하지 않는 테마 입니다."), @@ -12,7 +12,7 @@ public enum ThemeErrorCode implements ErrorCode { private final String message; - ThemeErrorCode(String message) { + ThemeErrors(String message) { this.message = message; } diff --git a/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java b/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java index 8b9a46312d..682aa1d221 100644 --- a/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java +++ b/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java @@ -11,7 +11,7 @@ class GlobalExceptionHandlerTest { void RoomescapeException을_에러_응답으로_변환한다() { // given GlobalExceptionHandler globalExceptionHandler = new GlobalExceptionHandler(); - BadRequestException exception = new BadRequestException(ReservationErrorCode.INVALID_RESERVATION_NAME); + BadRequestException exception = new BadRequestException(ReservationErrors.INVALID_RESERVATION_NAME); // when ResponseEntity response = globalExceptionHandler.handleBadRequestException(exception); From af36d71f49ddedf85edcb1e7f71d545fb32c25d6 Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 14 May 2026 17:11:08 +0900 Subject: [PATCH 16/43] =?UTF-8?q?refactor:=20=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=A1=B0=ED=9A=8C=20api=20url=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../domain/reservationtime/ReservationTimeController.java | 2 +- src/main/resources/static/js/times.js | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2764865bc9..db059bea78 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ ### 예약 시간 -#### `GET /times?themeId={themeId}&dateId={dateId}` +#### `GET /reservation-times/availability?themeId={themeId}&dateId={dateId}` - 설명: 특정 테마와 날짜의 예약 가능 시간 조회 - 응답 `200 OK` diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java index 211c6ca617..2ef9d6a560 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java @@ -56,7 +56,7 @@ public ResponseEntity deleteReservationTime(HttpServletRequest request, @P return ResponseEntity.ok().build(); } - @GetMapping("/times") + @GetMapping("/reservation-times/availability") public ResponseEntity> getReservationTimeAvailability( @RequestParam Long themeId, @RequestParam Long dateId diff --git a/src/main/resources/static/js/times.js b/src/main/resources/static/js/times.js index fcea075321..fc7763eeb8 100644 --- a/src/main/resources/static/js/times.js +++ b/src/main/resources/static/js/times.js @@ -376,7 +376,9 @@ document.addEventListener("DOMContentLoaded", () => { async function loadEditTimes(reservation, dateId) { try { - const times = await fetchJson(`/times?themeId=${reservation.theme.id}&dateId=${dateId}`); + const times = await fetchJson( + `/reservation-times/availability?themeId=${reservation.theme.id}&dateId=${dateId}` + ); state.editTimesByReservationId[reservation.reservationId] = times; renderUserReservations(); } catch (error) { @@ -409,7 +411,9 @@ document.addEventListener("DOMContentLoaded", () => { const selectedTheme = findTheme(); const selectedDate = findDate(); - state.times = await fetchJson(`/times?themeId=${state.selectedThemeId}&dateId=${state.selectedDateId}`); + state.times = await fetchJson( + `/reservation-times/availability?themeId=${state.selectedThemeId}&dateId=${state.selectedDateId}` + ); renderTimes(); selectedThemeInput.value = String(state.selectedThemeId); selectedDateInput.value = String(state.selectedDateId); From 248db1c6fa2f30bc5c25afaf3a400945d4c9210b Mon Sep 17 00:00:00 2001 From: Sumin Date: Thu, 14 May 2026 17:17:00 +0900 Subject: [PATCH 17/43] =?UTF-8?q?refactor:=20errors=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/domain/reservation/Reservation.java | 6 +++--- .../roomescape/domain/reservation/ReservationService.java | 8 ++++---- .../reservationdate/JdbcReservationDateRepository.java | 2 +- .../domain/reservationdate/ReservationDateService.java | 2 +- .../reservationtime/JdbcReservationTimeRepository.java | 2 +- .../domain/reservationtime/ReservationTime.java | 2 +- .../domain/reservationtime/ReservationTimeService.java | 2 +- .../java/roomescape/domain/theme/JdbcThemeRepository.java | 2 +- src/main/java/roomescape/domain/theme/ThemeService.java | 2 +- .../roomescape/support/exception/BadRequestException.java | 2 ++ .../roomescape/support/exception/ConflictException.java | 2 ++ .../java/roomescape/support/exception/ErrorResponse.java | 1 + .../support/exception/GlobalExceptionHandler.java | 1 + .../support/exception/InternalServerException.java | 2 ++ .../roomescape/support/exception/NotFoundException.java | 2 ++ .../roomescape/support/exception/RoomescapeException.java | 1 + .../roomescape/support/exception/{ => errors}/Errors.java | 2 +- .../exception/{ => errors}/ReservationDateErrors.java | 2 +- .../support/exception/{ => errors}/ReservationErrors.java | 2 +- .../exception/{ => errors}/ReservationTimeErrors.java | 2 +- .../support/exception/{ => errors}/RoomescapeErrors.java | 2 +- .../support/exception/{ => errors}/ThemeErrors.java | 2 +- .../support/exception/GlobalExceptionHandlerTest.java | 1 + 23 files changed, 32 insertions(+), 20 deletions(-) rename src/main/java/roomescape/support/exception/{ => errors}/Errors.java (63%) rename src/main/java/roomescape/support/exception/{ => errors}/ReservationDateErrors.java (93%) rename src/main/java/roomescape/support/exception/{ => errors}/ReservationErrors.java (93%) rename src/main/java/roomescape/support/exception/{ => errors}/ReservationTimeErrors.java (95%) rename src/main/java/roomescape/support/exception/{ => errors}/RoomescapeErrors.java (93%) rename src/main/java/roomescape/support/exception/{ => errors}/ThemeErrors.java (91%) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index fc9f5e2f2f..4cb5e1f85e 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -5,9 +5,9 @@ import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; import roomescape.support.exception.BadRequestException; -import roomescape.support.exception.ReservationErrors; -import roomescape.support.exception.ReservationTimeErrors; -import roomescape.support.exception.ThemeErrors; +import roomescape.support.exception.errors.ReservationErrors; +import roomescape.support.exception.errors.ReservationTimeErrors; +import roomescape.support.exception.errors.ThemeErrors; @Getter public class Reservation { diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 5e4ca78b45..d142b2b8d3 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -21,10 +21,10 @@ import roomescape.domain.theme.ThemeRepository; import roomescape.support.exception.BadRequestException; import roomescape.support.exception.NotFoundException; -import roomescape.support.exception.ReservationDateErrors; -import roomescape.support.exception.ReservationErrors; -import roomescape.support.exception.ReservationTimeErrors; -import roomescape.support.exception.ThemeErrors; +import roomescape.support.exception.errors.ReservationDateErrors; +import roomescape.support.exception.errors.ReservationErrors; +import roomescape.support.exception.errors.ReservationTimeErrors; +import roomescape.support.exception.errors.ThemeErrors; @Slf4j @Service diff --git a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java b/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java index 3ddfd5b932..a5b1927132 100644 --- a/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java +++ b/src/main/java/roomescape/domain/reservationdate/JdbcReservationDateRepository.java @@ -13,7 +13,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.RoomescapeErrors; +import roomescape.support.exception.errors.RoomescapeErrors; @Repository @RequiredArgsConstructor diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java index 6b59ab8e04..b9c35374ec 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java @@ -10,7 +10,7 @@ import roomescape.domain.reservationdate.dto.CreateReservationDateResponse; import roomescape.domain.reservationdate.dto.ReservationDateResponse; import roomescape.support.exception.ConflictException; -import roomescape.support.exception.ReservationDateErrors; +import roomescape.support.exception.errors.ReservationDateErrors; @Slf4j @Service diff --git a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java b/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java index 115cd191ec..1207ccb831 100644 --- a/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java +++ b/src/main/java/roomescape/domain/reservationtime/JdbcReservationTimeRepository.java @@ -13,7 +13,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.RoomescapeErrors; +import roomescape.support.exception.errors.RoomescapeErrors; @Repository @RequiredArgsConstructor diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java index bccfa37be9..13356a52aa 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTime.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTime.java @@ -3,7 +3,7 @@ import java.time.LocalTime; import lombok.Getter; import roomescape.support.exception.BadRequestException; -import roomescape.support.exception.ReservationTimeErrors; +import roomescape.support.exception.errors.ReservationTimeErrors; @Getter public class ReservationTime { diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java index f8ce3d7375..527872613d 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java @@ -12,7 +12,7 @@ import roomescape.domain.reservationtime.dto.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; import roomescape.domain.reservationtime.dto.ReservationTimeResponse; -import roomescape.support.exception.ReservationTimeErrors; +import roomescape.support.exception.errors.ReservationTimeErrors; @Slf4j @Service diff --git a/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java b/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java index 92fed41546..898a859af5 100644 --- a/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java +++ b/src/main/java/roomescape/domain/theme/JdbcThemeRepository.java @@ -11,7 +11,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import roomescape.support.exception.InternalServerException; -import roomescape.support.exception.RoomescapeErrors; +import roomescape.support.exception.errors.RoomescapeErrors; @Repository @RequiredArgsConstructor diff --git a/src/main/java/roomescape/domain/theme/ThemeService.java b/src/main/java/roomescape/domain/theme/ThemeService.java index 68876f2ba9..d9e30e468e 100644 --- a/src/main/java/roomescape/domain/theme/ThemeService.java +++ b/src/main/java/roomescape/domain/theme/ThemeService.java @@ -12,7 +12,7 @@ import roomescape.domain.theme.dto.ThemeRankResponse; import roomescape.domain.theme.dto.ThemeResponse; import roomescape.support.exception.ConflictException; -import roomescape.support.exception.ThemeErrors; +import roomescape.support.exception.errors.ThemeErrors; @Slf4j @Service diff --git a/src/main/java/roomescape/support/exception/BadRequestException.java b/src/main/java/roomescape/support/exception/BadRequestException.java index d17c80b8f5..4f9e1bb24a 100644 --- a/src/main/java/roomescape/support/exception/BadRequestException.java +++ b/src/main/java/roomescape/support/exception/BadRequestException.java @@ -1,5 +1,7 @@ package roomescape.support.exception; +import roomescape.support.exception.errors.Errors; + public class BadRequestException extends RoomescapeException { public BadRequestException(Errors errors, Object... args) { diff --git a/src/main/java/roomescape/support/exception/ConflictException.java b/src/main/java/roomescape/support/exception/ConflictException.java index 5007b4f637..8d3ca380a8 100644 --- a/src/main/java/roomescape/support/exception/ConflictException.java +++ b/src/main/java/roomescape/support/exception/ConflictException.java @@ -1,5 +1,7 @@ package roomescape.support.exception; +import roomescape.support.exception.errors.Errors; + public class ConflictException extends RoomescapeException { public ConflictException(Errors errors, Object... args) { diff --git a/src/main/java/roomescape/support/exception/ErrorResponse.java b/src/main/java/roomescape/support/exception/ErrorResponse.java index f5851c6a4f..9471d4fa32 100644 --- a/src/main/java/roomescape/support/exception/ErrorResponse.java +++ b/src/main/java/roomescape/support/exception/ErrorResponse.java @@ -2,6 +2,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import roomescape.support.exception.errors.Errors; public record ErrorResponse( String code, diff --git a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java index b7ef750988..24cb965a12 100644 --- a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import roomescape.support.exception.errors.RoomescapeErrors; @RestControllerAdvice public class GlobalExceptionHandler { diff --git a/src/main/java/roomescape/support/exception/InternalServerException.java b/src/main/java/roomescape/support/exception/InternalServerException.java index 2a2a43852a..b45387fc80 100644 --- a/src/main/java/roomescape/support/exception/InternalServerException.java +++ b/src/main/java/roomescape/support/exception/InternalServerException.java @@ -1,5 +1,7 @@ package roomescape.support.exception; +import roomescape.support.exception.errors.Errors; + public class InternalServerException extends RoomescapeException { public InternalServerException(Errors errors, Object... args) { diff --git a/src/main/java/roomescape/support/exception/NotFoundException.java b/src/main/java/roomescape/support/exception/NotFoundException.java index aef444a06d..acdf318fa4 100644 --- a/src/main/java/roomescape/support/exception/NotFoundException.java +++ b/src/main/java/roomescape/support/exception/NotFoundException.java @@ -1,5 +1,7 @@ package roomescape.support.exception; +import roomescape.support.exception.errors.Errors; + public class NotFoundException extends RoomescapeException { public NotFoundException(Errors errors, Object... args) { diff --git a/src/main/java/roomescape/support/exception/RoomescapeException.java b/src/main/java/roomescape/support/exception/RoomescapeException.java index d9d819befd..1eec4e317a 100644 --- a/src/main/java/roomescape/support/exception/RoomescapeException.java +++ b/src/main/java/roomescape/support/exception/RoomescapeException.java @@ -1,6 +1,7 @@ package roomescape.support.exception; import lombok.Getter; +import roomescape.support.exception.errors.Errors; @Getter public abstract class RoomescapeException extends RuntimeException { diff --git a/src/main/java/roomescape/support/exception/Errors.java b/src/main/java/roomescape/support/exception/errors/Errors.java similarity index 63% rename from src/main/java/roomescape/support/exception/Errors.java rename to src/main/java/roomescape/support/exception/errors/Errors.java index 5db80e0fa0..28c45d8560 100644 --- a/src/main/java/roomescape/support/exception/Errors.java +++ b/src/main/java/roomescape/support/exception/errors/Errors.java @@ -1,4 +1,4 @@ -package roomescape.support.exception; +package roomescape.support.exception.errors; public interface Errors { diff --git a/src/main/java/roomescape/support/exception/ReservationDateErrors.java b/src/main/java/roomescape/support/exception/errors/ReservationDateErrors.java similarity index 93% rename from src/main/java/roomescape/support/exception/ReservationDateErrors.java rename to src/main/java/roomescape/support/exception/errors/ReservationDateErrors.java index 77bf6100d9..697da3d75f 100644 --- a/src/main/java/roomescape/support/exception/ReservationDateErrors.java +++ b/src/main/java/roomescape/support/exception/errors/ReservationDateErrors.java @@ -1,4 +1,4 @@ -package roomescape.support.exception; +package roomescape.support.exception.errors; import lombok.Getter; diff --git a/src/main/java/roomescape/support/exception/ReservationErrors.java b/src/main/java/roomescape/support/exception/errors/ReservationErrors.java similarity index 93% rename from src/main/java/roomescape/support/exception/ReservationErrors.java rename to src/main/java/roomescape/support/exception/errors/ReservationErrors.java index 4307ec1206..f7dfd08d31 100644 --- a/src/main/java/roomescape/support/exception/ReservationErrors.java +++ b/src/main/java/roomescape/support/exception/errors/ReservationErrors.java @@ -1,4 +1,4 @@ -package roomescape.support.exception; +package roomescape.support.exception.errors; import lombok.Getter; diff --git a/src/main/java/roomescape/support/exception/ReservationTimeErrors.java b/src/main/java/roomescape/support/exception/errors/ReservationTimeErrors.java similarity index 95% rename from src/main/java/roomescape/support/exception/ReservationTimeErrors.java rename to src/main/java/roomescape/support/exception/errors/ReservationTimeErrors.java index c39a0633c4..0a7ecbffa1 100644 --- a/src/main/java/roomescape/support/exception/ReservationTimeErrors.java +++ b/src/main/java/roomescape/support/exception/errors/ReservationTimeErrors.java @@ -1,4 +1,4 @@ -package roomescape.support.exception; +package roomescape.support.exception.errors; import lombok.Getter; diff --git a/src/main/java/roomescape/support/exception/RoomescapeErrors.java b/src/main/java/roomescape/support/exception/errors/RoomescapeErrors.java similarity index 93% rename from src/main/java/roomescape/support/exception/RoomescapeErrors.java rename to src/main/java/roomescape/support/exception/errors/RoomescapeErrors.java index beb7ad6e41..0489300ec1 100644 --- a/src/main/java/roomescape/support/exception/RoomescapeErrors.java +++ b/src/main/java/roomescape/support/exception/errors/RoomescapeErrors.java @@ -1,4 +1,4 @@ -package roomescape.support.exception; +package roomescape.support.exception.errors; import lombok.Getter; diff --git a/src/main/java/roomescape/support/exception/ThemeErrors.java b/src/main/java/roomescape/support/exception/errors/ThemeErrors.java similarity index 91% rename from src/main/java/roomescape/support/exception/ThemeErrors.java rename to src/main/java/roomescape/support/exception/errors/ThemeErrors.java index 373361f37e..471d147753 100644 --- a/src/main/java/roomescape/support/exception/ThemeErrors.java +++ b/src/main/java/roomescape/support/exception/errors/ThemeErrors.java @@ -1,4 +1,4 @@ -package roomescape.support.exception; +package roomescape.support.exception.errors; import lombok.Getter; diff --git a/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java b/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java index 682aa1d221..c81768664f 100644 --- a/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java +++ b/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; +import roomescape.support.exception.errors.ReservationErrors; class GlobalExceptionHandlerTest { From 22eb9e0cf0114f39946475815a9f59ca9fa40345 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 20:07:34 +0900 Subject: [PATCH 18/43] =?UTF-8?q?refactor:=20=EC=97=A3=EC=A7=80=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/Reservation.java | 5 ++++ .../reservation/ReservationController.java | 13 ++++++-- .../dto/CreateReservationRequest.java | 4 +-- .../exception/GlobalExceptionHandler.java | 30 ++++++++++++++++++- .../exception/errors/ReservationErrors.java | 1 + .../exception/errors/RoomescapeErrors.java | 2 ++ .../domain/reservation/ReservationTest.java | 14 +++++++++ 7 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/Reservation.java b/src/main/java/roomescape/domain/reservation/Reservation.java index 4cb5e1f85e..5325224053 100644 --- a/src/main/java/roomescape/domain/reservation/Reservation.java +++ b/src/main/java/roomescape/domain/reservation/Reservation.java @@ -12,6 +12,8 @@ @Getter public class Reservation { + private static final int MAX_NAME_LENGTH = 10; + private final Long id; private final String name; private final ReservationDate date; @@ -78,6 +80,9 @@ private static void validate(String name, ReservationDate date, ReservationTime if (name == null || name.isBlank()) { throw new BadRequestException(ReservationErrors.INVALID_RESERVATION_NAME); } + if (name.length() > MAX_NAME_LENGTH) { + throw new BadRequestException(ReservationErrors.INVALID_RESERVATION_NAME_LENGTH); + } if (date == null) { throw new BadRequestException(ReservationErrors.INVALID_RESERVATION_DATE); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index 76cf1f1bcf..e2426e8292 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -1,9 +1,11 @@ package roomescape.domain.reservation; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -17,6 +19,7 @@ import roomescape.domain.reservation.dto.UpdateReservationRequest; import roomescape.domain.reservation.dto.UserReservationResponse; +@Validated @RestController @RequiredArgsConstructor public class ReservationController { @@ -32,13 +35,19 @@ public ResponseEntity createReservation( } @GetMapping("/reservations") - public ResponseEntity getUserReservations(@RequestParam String name) { + public ResponseEntity getUserReservations( + @RequestParam + @NotBlank(message = "예약자 이름은 필수 입력값 입니다.") + String name + ) { UserReservationResponse response = reservationService.getUserReservations(name); return ResponseEntity.ok(response); } @DeleteMapping("/reservations/{id}") - public ResponseEntity deleteUserReservation(@PathVariable Long id) { + public ResponseEntity deleteUserReservation( + @PathVariable Long id + ) { reservationService.deleteUserReservation(id); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } diff --git a/src/main/java/roomescape/domain/reservation/dto/CreateReservationRequest.java b/src/main/java/roomescape/domain/reservation/dto/CreateReservationRequest.java index 5ac0166649..897c4b402a 100644 --- a/src/main/java/roomescape/domain/reservation/dto/CreateReservationRequest.java +++ b/src/main/java/roomescape/domain/reservation/dto/CreateReservationRequest.java @@ -2,15 +2,13 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; public record CreateReservationRequest( - @Size(max = 10, message = "이름은 10자 이하여야 합니다.") - @NotBlank(message = "이름은 비어있을 수 없습니다. 10자 이내의 이름을 입력해주세요.") + @NotBlank(message = "이름은 비어있을 수 없습니다.") String name, @NotNull(message = "날짜는 필수 선택 사항 입니다. 날짜를 선택해주세요.") diff --git a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java index 24cb965a12..378aa3b6bd 100644 --- a/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/support/exception/GlobalExceptionHandler.java @@ -1,10 +1,14 @@ package roomescape.support.exception; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.ObjectError; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import roomescape.support.exception.errors.RoomescapeErrors; @@ -34,7 +38,6 @@ public ResponseEntity handleInternalServerException(InternalServe @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { - String message = e.getAllErrors() .stream() .findFirst() @@ -51,6 +54,31 @@ public ResponseEntity handleHttpMessageNotReadableException( return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrors.INPUT_FORMAT_ERROR); } + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolationException(ConstraintViolationException e) { + String message = e.getConstraintViolations() + .stream() + .findFirst() + .map(ConstraintViolation::getMessage) + .orElse(RoomescapeErrors.INPUT_VALIDATION_ERROR.getMessage()); + + return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrors.INPUT_VALIDATION_ERROR, message); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissingServletRequestParameterException( + MissingServletRequestParameterException e + ) { + return ErrorResponse.of(HttpStatus.BAD_REQUEST, RoomescapeErrors.REQUIRED_PARAMETER_MISSING); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity handleHttpRequestMethodNotSupportedException( + HttpRequestMethodNotSupportedException e + ) { + return ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED, RoomescapeErrors.METHOD_NOT_ALLOWED); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, RoomescapeErrors.INTERNAL_SERVER_ERROR); diff --git a/src/main/java/roomescape/support/exception/errors/ReservationErrors.java b/src/main/java/roomescape/support/exception/errors/ReservationErrors.java index f7dfd08d31..3600df9357 100644 --- a/src/main/java/roomescape/support/exception/errors/ReservationErrors.java +++ b/src/main/java/roomescape/support/exception/errors/ReservationErrors.java @@ -6,6 +6,7 @@ public enum ReservationErrors implements Errors { INVALID_RESERVATION_NAME("이름은 비어 있을 수 없습니다."), + INVALID_RESERVATION_NAME_LENGTH("이름은 10자 이하여야 합니다."), INVALID_RESERVATION_DATE("날짜는 필수입니다."), RESERVATION_NOT_FOUND("존재하지 않는 예약건 입니다"), DUPLICATED_RESERVATION("중복 예약입니다. 예약 정보를 다시 확인해주세요."), diff --git a/src/main/java/roomescape/support/exception/errors/RoomescapeErrors.java b/src/main/java/roomescape/support/exception/errors/RoomescapeErrors.java index 0489300ec1..bfeff25b4a 100644 --- a/src/main/java/roomescape/support/exception/errors/RoomescapeErrors.java +++ b/src/main/java/roomescape/support/exception/errors/RoomescapeErrors.java @@ -9,6 +9,8 @@ public enum RoomescapeErrors implements Errors { INPUT_VALIDATION_ERROR("입력 검증 오류가 발생했습니다."), INTERNAL_SERVER_ERROR("서버 내부 오류가 발생했습니다."), INVALID_GENERATED_KEY("생성 키를 조회할 수 없습니다."), + REQUIRED_PARAMETER_MISSING("필수 요청 파라미터가 누락되었습니다."), + METHOD_NOT_ALLOWED("요청한 경로에서 지원하지 않는 HTTP 메서드입니다."), ; private final String message; diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index c1c695ff49..475d486203 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -120,6 +120,20 @@ class ReservationTest { .hasMessage("이름은 비어 있을 수 없습니다."); } + @Test + void 이름이_10자를_초과하면_예외가_발생한다() { + // given + String name = "보예보예보예보예보예보"; + 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("이름은 10자 이하여야 합니다."); + } + @Test void 날짜가_null이면_예외가_발생한다() { // given From 33bfaaa5eb04b0cb366b1cea81dff7e68bed0860 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 21:11:31 +0900 Subject: [PATCH 19/43] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20`@Transactional`=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/domain/reservation/ReservationService.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index d142b2b8d3..51d230e4f7 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -7,7 +7,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import roomescape.domain.reservation.admin.dto.ReservationResponse; import roomescape.domain.reservation.dto.CreateReservationRequest; import roomescape.domain.reservation.dto.CreateReservationResponse; @@ -28,7 +27,6 @@ @Slf4j @Service -@Transactional(readOnly = true) @RequiredArgsConstructor public class ReservationService { @@ -38,7 +36,6 @@ public class ReservationService { private final ThemeRepository themeRepository; private final Clock clock; - @Transactional public CreateReservationResponse createReservation(CreateReservationRequest request) { ReservationTime reservationTime = reservationTimeRepository.findById(request.timeId()) .orElseThrow(() -> new NotFoundException(ReservationTimeErrors.RESERVATION_TIME_NOT_EXIST)); @@ -64,7 +61,6 @@ public UserReservationResponse getUserReservations(String name) { return UserReservationResponse.of(name, reservations); } - @Transactional public void deleteReservationByAdmin(Long id) { int deletedCount = reservationRepository.deleteById(id); if (deletedCount == 0) { @@ -72,7 +68,6 @@ public void deleteReservationByAdmin(Long id) { } } - @Transactional public void deleteUserReservation(Long id) { Reservation reservation = reservationRepository.findById(id) .orElseThrow(() -> new NotFoundException(ReservationErrors.RESERVATION_NOT_FOUND)); @@ -80,7 +75,6 @@ public void deleteUserReservation(Long id) { reservationRepository.deleteById(id); } - @Transactional public void updateReservation(Long id, UpdateReservationRequest request) { Reservation reservation = reservationRepository.findById(id) .orElseThrow(() -> new NotFoundException(ReservationErrors.RESERVATION_NOT_FOUND)); From 07f4ed36b6a66d56c9ab292ea634f124d5d0ecb8 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 21:16:34 +0900 Subject: [PATCH 20/43] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EA=B9=85=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/domain/reservation/ReservationService.java | 7 +------ .../domain/reservationdate/ReservationDateService.java | 7 +------ .../domain/reservationtime/ReservationTimeService.java | 7 +------ src/main/java/roomescape/domain/theme/ThemeService.java | 7 +------ 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 51d230e4f7..657328b444 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -5,7 +5,6 @@ import java.time.LocalTime; import java.util.List; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import roomescape.domain.reservation.admin.dto.ReservationResponse; import roomescape.domain.reservation.dto.CreateReservationRequest; @@ -25,7 +24,6 @@ import roomescape.support.exception.errors.ReservationTimeErrors; import roomescape.support.exception.errors.ThemeErrors; -@Slf4j @Service @RequiredArgsConstructor public class ReservationService { @@ -62,10 +60,7 @@ public UserReservationResponse getUserReservations(String name) { } public void deleteReservationByAdmin(Long id) { - int deletedCount = reservationRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("이미 삭제된 예약 삭제 요청이 들어왔습니다. reservationId={}", id); - } + reservationRepository.deleteById(id); } public void deleteUserReservation(Long id) { diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java index b9c35374ec..6bcbb45faf 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java @@ -2,7 +2,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.dto.AdminReservationDateResponse; @@ -12,7 +11,6 @@ import roomescape.support.exception.ConflictException; import roomescape.support.exception.errors.ReservationDateErrors; -@Slf4j @Service @RequiredArgsConstructor public class ReservationDateService { @@ -35,10 +33,7 @@ public void deleteReservationDate(Long id) { if (reservationRepository.countByReservationDateId(id) > 0) { throw new ConflictException(ReservationDateErrors.RESERVATION_DATE_IN_USE); } - int deletedCount = reservationDateRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("이미 삭제된 날짜의 삭제 요청이 들어왔습니다. dateId={}", id); - } + reservationDateRepository.deleteById(id); } public List getAllReservationDate() { diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java index 527872613d..7bed675603 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; import roomescape.support.exception.ConflictException; @@ -14,7 +13,6 @@ import roomescape.domain.reservationtime.dto.ReservationTimeResponse; import roomescape.support.exception.errors.ReservationTimeErrors; -@Slf4j @Service @RequiredArgsConstructor public class ReservationTimeService { @@ -37,10 +35,7 @@ public void deleteReservationTime(Long id) { if (reservationRepository.countByTimeId(id) > 0) { throw new ConflictException(ReservationTimeErrors.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/ThemeService.java b/src/main/java/roomescape/domain/theme/ThemeService.java index d9e30e468e..15a279f8ea 100644 --- a/src/main/java/roomescape/domain/theme/ThemeService.java +++ b/src/main/java/roomescape/domain/theme/ThemeService.java @@ -3,7 +3,6 @@ import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.theme.dto.AdminThemeResponse; @@ -14,7 +13,6 @@ import roomescape.support.exception.ConflictException; import roomescape.support.exception.errors.ThemeErrors; -@Slf4j @Service @RequiredArgsConstructor public class ThemeService { @@ -40,10 +38,7 @@ public void deleteTheme(Long id) { if (reservationRepository.countByThemeId(id) > 0) { throw new ConflictException(ThemeErrors.THEME_IN_USE); } - int deletedCount = themeRepository.deleteById(id); - if (deletedCount == 0) { - log.warn("삭제할 테마가 존재하지 않습니다. themeId = {}", id); - } + themeRepository.deleteById(id); } public List getAllTheme() { From fc2b64da4af3591b2884b0f502d0b2be904f2b31 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 21:20:36 +0900 Subject: [PATCH 21/43] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservation/ReservationController.java | 2 +- .../roomescape/domain/reservation/ReservationService.java | 4 ++-- .../reservation/admin/AdminReservationController.java | 2 +- .../domain/reservation/ReservationServiceTest.java | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index e2426e8292..c572ceadc0 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -48,7 +48,7 @@ public ResponseEntity getUserReservations( public ResponseEntity deleteUserReservation( @PathVariable Long id ) { - reservationService.deleteUserReservation(id); + reservationService.cancelUserReservation(id); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 657328b444..77b182e7f3 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -59,11 +59,11 @@ public UserReservationResponse getUserReservations(String name) { return UserReservationResponse.of(name, reservations); } - public void deleteReservationByAdmin(Long id) { + public void cancelReservationByAdmin(Long id) { reservationRepository.deleteById(id); } - public void deleteUserReservation(Long id) { + public void cancelUserReservation(Long id) { Reservation reservation = reservationRepository.findById(id) .orElseThrow(() -> new NotFoundException(ReservationErrors.RESERVATION_NOT_FOUND)); validateUserCanDeleteReservation(reservation); diff --git a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java index 52e725bdef..519b3fedc4 100644 --- a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java +++ b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java @@ -34,7 +34,7 @@ public ResponseEntity deleteReservation(HttpServletRequest request, @PathV if (validator.isUnauthorized(request)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - reservationService.deleteReservationByAdmin(id); + reservationService.cancelReservationByAdmin(id); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } } diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index e4b31aaeae..ca9a441ad4 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -426,7 +426,7 @@ void setUp() { ); // when - reservationService.deleteUserReservation(savedReservation.getId()); + reservationService.cancelUserReservation(savedReservation.getId()); // then assertThat(reservationRepository.findById(savedReservation.getId())).isEmpty(); @@ -455,7 +455,7 @@ void setUp() { ); // when & then - assertThatThrownBy(() -> reservationService.deleteUserReservation(savedReservation.getId())) + assertThatThrownBy(() -> reservationService.cancelUserReservation(savedReservation.getId())) .isInstanceOf(BadRequestException.class) .hasMessage("현재보다 이전 시간 예약을 삭제할 수 없습니다. 현재 시각:" + LocalTime.of(13, 0)); } @@ -483,7 +483,7 @@ void setUp() { ); // when & then - assertThatThrownBy(() -> reservationService.deleteUserReservation(savedReservation.getId())) + assertThatThrownBy(() -> reservationService.cancelUserReservation(savedReservation.getId())) .isInstanceOf(BadRequestException.class) .hasMessage("예전 예약은 삭제할 수 없습니다. 오늘 날짜:" + LocalDate.of(2026, 5, 12)); } @@ -501,7 +501,7 @@ void setUp() { ); // when & then - assertThatThrownBy(() -> reservationService.deleteUserReservation(1L)) + assertThatThrownBy(() -> reservationService.cancelUserReservation(1L)) .isInstanceOf(RoomescapeException.class) .hasMessage("존재하지 않는 예약건 입니다"); } From b199646057e35e34bf25a481dd65de65be0cb942 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 21:24:24 +0900 Subject: [PATCH 22/43] =?UTF-8?q?refactor:=20=EB=82=A0=EC=A7=9C=EC=99=80?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=20=EC=B6=94=EC=B6=9C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationService.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationService.java b/src/main/java/roomescape/domain/reservation/ReservationService.java index 77b182e7f3..2447dc2c85 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationService.java +++ b/src/main/java/roomescape/domain/reservation/ReservationService.java @@ -2,6 +2,7 @@ import java.time.Clock; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import lombok.RequiredArgsConstructor; @@ -93,13 +94,14 @@ private void validateReservationScheduleToCreate( ReservationDate reservationDate, ReservationTime reservationTime ) { - LocalDate today = LocalDate.now(clock); - LocalTime now = LocalTime.now(clock); + LocalDateTime now = LocalDateTime.now(clock); + LocalDate today = now.toLocalDate(); + LocalTime currentTime = now.toLocalTime(); if (isPastDate(reservationDate, today)) { throw new BadRequestException(ReservationDateErrors.RESERVATION_DATE_MUST_BE_TODAY_OR_LATER, today); } - if (isPastTimeToday(reservationDate, reservationTime, today, now)) { - throw new BadRequestException(ReservationTimeErrors.RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER, now); + if (isPastTimeToday(reservationDate, reservationTime, today, currentTime)) { + throw new BadRequestException(ReservationTimeErrors.RESERVATION_TIME_SHOULD_BE_NOW_OR_LATER, currentTime); } } @@ -130,13 +132,14 @@ private void validateDuplicatedWithOther( } private void validateUserCanDeleteReservation(Reservation reservation) { - LocalDate today = LocalDate.now(clock); - LocalTime now = LocalTime.now(clock); + LocalDateTime now = LocalDateTime.now(clock); + LocalDate today = now.toLocalDate(); + LocalTime currentTime = now.toLocalTime(); if (isPastDate(reservation.getDate(), today)) { throw new BadRequestException(ReservationDateErrors.PAST_RESERVATION_DATE_CANNOT_BE_DELETED, today); } - if (isPastTimeToday(reservation.getDate(), reservation.getTime(), today, now)) { - throw new BadRequestException(ReservationTimeErrors.PAST_RESERVATION_TiME_CANNOT_BE_DELETED, now); + if (isPastTimeToday(reservation.getDate(), reservation.getTime(), today, currentTime)) { + throw new BadRequestException(ReservationTimeErrors.PAST_RESERVATION_TiME_CANNOT_BE_DELETED, currentTime); } } From 62c27a931daee0492f7d0ee9608d9a9f121fbe5e Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 22:38:28 +0900 Subject: [PATCH 23/43] =?UTF-8?q?refactor:=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20theme=20id=EB=B0=8F=20date=20i?= =?UTF-8?q?d=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationTimeService.java | 17 ++- .../ReservationTimeServiceTest.java | 100 +++++++++++++++--- 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java index 7bed675603..919d07ea9b 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java @@ -6,12 +6,17 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; -import roomescape.support.exception.ConflictException; +import roomescape.domain.reservationdate.ReservationDateRepository; import roomescape.domain.reservationtime.dto.CreateTimeRequest; import roomescape.domain.reservationtime.dto.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; import roomescape.domain.reservationtime.dto.ReservationTimeResponse; +import roomescape.domain.theme.ThemeRepository; +import roomescape.support.exception.ConflictException; +import roomescape.support.exception.NotFoundException; +import roomescape.support.exception.errors.ReservationDateErrors; import roomescape.support.exception.errors.ReservationTimeErrors; +import roomescape.support.exception.errors.ThemeErrors; @Service @RequiredArgsConstructor @@ -19,6 +24,8 @@ public class ReservationTimeService { private final ReservationTimeRepository reservationTimeRepository; private final ReservationRepository reservationRepository; + private final ThemeRepository themeRepository; + private final ReservationDateRepository reservationDateRepository; public CreateTimeResponse createReservationTime(CreateTimeRequest request) { ReservationTime reservationTime = reservationTimeRepository.save(request.toEntity()); @@ -39,6 +46,7 @@ public void deleteReservationTime(Long id) { } public List getReservationTimeAvailability(Long themeId, Long dateId) { + validateThemeAndDateExists(themeId, dateId); List allReservationTime = reservationTimeRepository.findAll(); Set reservedTimeIds = getReservedTimeIds(themeId, dateId); return allReservationTime.stream() @@ -49,6 +57,13 @@ public List getReservationTimeAvailability( .toList(); } + private void validateThemeAndDateExists(Long themeId, Long dateId) { + themeRepository.findById(themeId) + .orElseThrow(() -> new NotFoundException(ThemeErrors.THEME_NOT_EXIST)); + reservationDateRepository.findById(dateId) + .orElseThrow(() -> new NotFoundException(ReservationDateErrors.RESERVATION_DATE_NOT_EXIST)); + } + private Set getReservedTimeIds(Long themeId, Long dateId) { List reservedTimeIds = reservationRepository.findReservedTimes(themeId, dateId); return new HashSet<>(reservedTimeIds); diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java index 6937263d8a..4f09c79703 100644 --- a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java @@ -14,30 +14,35 @@ import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.dto.CreateTimeRequest; import roomescape.domain.reservationtime.dto.CreateTimeResponse; +import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; import roomescape.domain.reservationtime.dto.ReservationTimeResponse; import roomescape.domain.theme.Theme; +import roomescape.support.exception.NotFoundException; import roomescape.support.exception.RoomescapeException; +import roomescape.support.fake.FakeReservationDateRepository; import roomescape.support.fake.FakeReservationRepository; import roomescape.support.fake.FakeReservationTimeRepository; +import roomescape.support.fake.FakeThemeRepository; class ReservationTimeServiceTest { private FakeReservationRepository reservationRepository; private FakeReservationTimeRepository reservationTimeRepository; + private FakeThemeRepository themeRepository; + private FakeReservationDateRepository reservationDateRepository; @BeforeEach void setUp() { reservationRepository = new FakeReservationRepository(); reservationTimeRepository = new FakeReservationTimeRepository(); + themeRepository = new FakeThemeRepository(); + reservationDateRepository = new FakeReservationDateRepository(); } @Test void 예약_시간을_생성한다() { // given - ReservationTimeService reservationTimeService = new ReservationTimeService( - reservationTimeRepository, - reservationRepository - ); + ReservationTimeService reservationTimeService = createReservationTimeService(); // when CreateTimeResponse response = reservationTimeService.createReservationTime( @@ -58,10 +63,7 @@ void setUp() { // given reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(10, 0))); reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(11, 0))); - ReservationTimeService reservationTimeService = new ReservationTimeService( - reservationTimeRepository, - reservationRepository - ); + ReservationTimeService reservationTimeService = createReservationTimeService(); // when List responses = reservationTimeService.getAllReservationTime(); @@ -92,10 +94,7 @@ void setUp() { Theme.of(1L, "공포", "무서운 테마", "theme-url") ) ); - ReservationTimeService reservationTimeService = new ReservationTimeService( - reservationTimeRepository, - reservationRepository - ); + ReservationTimeService reservationTimeService = createReservationTimeService(); // when & then assertThatThrownBy(() -> reservationTimeService.deleteReservationTime(reservationTime.getId())) @@ -109,10 +108,7 @@ void setUp() { ReservationTime reservationTime = reservationTimeRepository.save( ReservationTime.createWithoutId(LocalTime.of(10, 0)) ); - ReservationTimeService reservationTimeService = new ReservationTimeService( - reservationTimeRepository, - reservationRepository - ); + ReservationTimeService reservationTimeService = createReservationTimeService(); // when reservationTimeService.deleteReservationTime(reservationTime.getId()); @@ -120,4 +116,76 @@ void setUp() { // then assertThat(reservationTimeRepository.findById(reservationTime.getId())).isEmpty(); } + + @Test + void 예약_가능_시간을_조회한다() { + // given + ReservationTime firstReservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(10, 0)) + ); + ReservationTime secondReservationTime = reservationTimeRepository.save( + ReservationTime.createWithoutId(LocalTime.of(11, 0)) + ); + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 16)) + ); + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + reservationRepository.save( + Reservation.createWithoutId("보예", reservationDate, firstReservationTime, theme) + ); + ReservationTimeService reservationTimeService = createReservationTimeService(); + + // when + List responses = reservationTimeService.getReservationTimeAvailability( + theme.getId(), + reservationDate.getId() + ); + + // then + assertThat(responses) + .extracting( + ReservationTimeAvailabilityResponse::timeId, + ReservationTimeAvailabilityResponse::startAt, + ReservationTimeAvailabilityResponse::available + ) + .containsExactly( + tuple(firstReservationTime.getId(), LocalTime.of(10, 0), false), + tuple(secondReservationTime.getId(), LocalTime.of(11, 0), true) + ); + } + + @Test + void 존재하지_않는_테마로_예약_가능_시간을_조회할_수_없다() { + // given + ReservationDate reservationDate = reservationDateRepository.save( + ReservationDate.createWithoutId(LocalDate.of(2026, 5, 16)) + ); + ReservationTimeService reservationTimeService = createReservationTimeService(); + + // when & then + assertThatThrownBy(() -> reservationTimeService.getReservationTimeAvailability(1L, reservationDate.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 테마 입니다."); + } + + @Test + void 존재하지_않는_날짜로_예약_가능_시간을_조회할_수_없다() { + // given + Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); + ReservationTimeService reservationTimeService = createReservationTimeService(); + + // when & then + assertThatThrownBy(() -> reservationTimeService.getReservationTimeAvailability(theme.getId(), 1L)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 날짜 입니다."); + } + + private ReservationTimeService createReservationTimeService() { + return new ReservationTimeService( + reservationTimeRepository, + reservationRepository, + themeRepository, + reservationDateRepository + ); + } } From e84bd8ad345cc736762280c64c2fb8e89f7dbf9d Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 23:01:30 +0900 Subject: [PATCH 24/43] =?UTF-8?q?refactor:=20=EB=8F=99=EC=9D=BC=20?= =?UTF-8?q?=EC=9E=90=EB=A3=8C=ED=98=95=20=EA=B2=80=EC=A6=9D=20=ED=98=95?= =?UTF-8?q?=ED=83=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/domain/theme/Theme.java | 11 +++++++++++ .../domain/theme/dto/CreateThemeRequest.java | 9 +++------ .../support/exception/errors/ThemeErrors.java | 1 + 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/roomescape/domain/theme/Theme.java b/src/main/java/roomescape/domain/theme/Theme.java index f87c4675a7..26f9e21328 100644 --- a/src/main/java/roomescape/domain/theme/Theme.java +++ b/src/main/java/roomescape/domain/theme/Theme.java @@ -1,16 +1,21 @@ package roomescape.domain.theme; import lombok.Getter; +import roomescape.support.exception.BadRequestException; +import roomescape.support.exception.errors.ThemeErrors; @Getter public class Theme { + private static final int MAX_NAME_LENGTH = 10; + private final Long id; private final String name; private final String content; private final String url; private Theme(Long id, String name, String content, String url) { + validateName(name); this.id = id; this.name = name; this.content = content; @@ -34,4 +39,10 @@ public static Theme createWithoutId(String name, String content, String url) { url ); } + + private void validateName(String name) { + if (name.length() > MAX_NAME_LENGTH) { + throw new BadRequestException(ThemeErrors.INVALID_THEME_NAME_LENGTH); + } + } } diff --git a/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java b/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java index 9f092227d8..421b98b983 100644 --- a/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java +++ b/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java @@ -1,19 +1,16 @@ package roomescape.domain.theme.dto; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import roomescape.domain.theme.Theme; public record CreateThemeRequest( - @Size(max = 10, message = "이름은 10자 이하여야 합니다. 다시 입력해주세요.") - @NotBlank(message = "이름은 비어있을 수 없습니다. 10자 이하의 이름을 입력해주세요.") + @NotBlank(message = "테마 이름은 비어있을 수 없습니다.") String name, - @NotNull(message = "테마 내용은 필수 입력값 입니다. 테마에 대한 설명을 입력해주세요.") + @NotBlank(message = "테마 내용은 비어있을 수 없습니다.") String content, - @NotNull(message = "url은 비어있을 수 없습니다. url을 입력해주세요.") + @NotBlank(message = "테마 URL은 비어있을 수 없습니다.") String url ) { diff --git a/src/main/java/roomescape/support/exception/errors/ThemeErrors.java b/src/main/java/roomescape/support/exception/errors/ThemeErrors.java index 471d147753..be5defc4dc 100644 --- a/src/main/java/roomescape/support/exception/errors/ThemeErrors.java +++ b/src/main/java/roomescape/support/exception/errors/ThemeErrors.java @@ -6,6 +6,7 @@ public enum ThemeErrors implements Errors { INVALID_THEME("테마는 필수입니다."), + INVALID_THEME_NAME_LENGTH("테마 이름은 10자 이하여야 합니다."), THEME_NOT_EXIST("존재하지 않는 테마 입니다."), THEME_IN_USE("이미 예약이 존재하는 테마는 삭제할 수 없습니다."), ; From 54adb7beb7bf7a678b9e7207edc2d9d49ee3e9de Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 23:01:46 +0900 Subject: [PATCH 25/43] =?UTF-8?q?test:=20=ED=85=8C=EB=A7=88=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/domain/theme/ThemeTest.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/test/java/roomescape/domain/theme/ThemeTest.java diff --git a/src/test/java/roomescape/domain/theme/ThemeTest.java b/src/test/java/roomescape/domain/theme/ThemeTest.java new file mode 100644 index 0000000000..9b6b71f52b --- /dev/null +++ b/src/test/java/roomescape/domain/theme/ThemeTest.java @@ -0,0 +1,41 @@ +package roomescape.domain.theme; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import org.junit.jupiter.api.Test; +import roomescape.support.exception.RoomescapeException; + +class ThemeTest { + + @Test + void id가_없는_테마를_생성한다() { + // given + String name = "미스터리"; + String content = "보예의 미스터리"; + String url = "theme-url"; + + // when + Theme theme = Theme.createWithoutId(name, content, url); + + // then + assertSoftly(softly -> { + assertThat(theme.getId()).isNull(); + assertThat(theme.getName()).isEqualTo(name); + assertThat(theme.getContent()).isEqualTo(content); + assertThat(theme.getUrl()).isEqualTo(url); + }); + } + + @Test + void 이름이_10자를_초과하면_예외가_발생한다() { + // given + String name = "공포공포공포공포공포공"; + + // when & then + assertThatThrownBy(() -> Theme.createWithoutId(name, "보예의 미스터리", "theme-url")) + .isInstanceOf(RoomescapeException.class) + .hasMessage("테마 이름은 10자 이하여야 합니다."); + } +} From 991c1085e510fbafa184c9e1603547cbb4fb7bac Mon Sep 17 00:00:00 2001 From: Sumin Date: Sat, 16 May 2026 23:11:13 +0900 Subject: [PATCH 26/43] =?UTF-8?q?refactor:=20=EC=BF=BC=EB=A6=AC=EB=AC=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JdbcReservationRepository.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java index a45e1f95e5..fda6f94557 100644 --- a/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java +++ b/src/main/java/roomescape/domain/reservation/JdbcReservationRepository.java @@ -86,17 +86,21 @@ select count(*) from reservation where theme_id = ? """; - private static final String COUNT_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL = + private static final String EXISTS_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL = """ - select count(*) - from reservation r - where time_id = ? and date_id = ? and theme_id = ? + select exists( + select 1 + from reservation r + where time_id = ? and date_id = ? and theme_id = ? + ) """; - private static final String COUNT_OTHER_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL = + private static final String EXISTS_OTHER_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL = """ - select count(*) - from reservation r - where r.id <> ? and time_id = ? and date_id = ? and theme_id = ? + select exists( + select 1 + from reservation r + where r.id <> ? and time_id = ? and date_id = ? and theme_id = ? + ) """; private static final String FIND_BY_NAME_SQL = """ @@ -196,27 +200,27 @@ public int countByThemeId(Long themeId) { @Override public boolean existsReservation(Long timeId, Long dateId, Long themeId) { - Integer count = jdbcTemplate.queryForObject( - COUNT_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL, - Integer.class, + Boolean exists = jdbcTemplate.queryForObject( + EXISTS_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL, + Boolean.class, timeId, dateId, themeId ); - return count != null && count > 0; + return exists != null && exists; } @Override public boolean existsOtherReservation(Long id, Long timeId, Long dateId, Long themeId) { - Integer count = jdbcTemplate.queryForObject( - COUNT_OTHER_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL, - Integer.class, + Boolean exists = jdbcTemplate.queryForObject( + EXISTS_OTHER_RESERVATION_BY_TIME_AND_DATE_AND_THEME_SQL, + Boolean.class, id, timeId, dateId, themeId ); - return count != null && count > 0; + return exists != null && exists; } @Override From 71f01e8f518bd098e92f97d476dd03fff1e652ca Mon Sep 17 00:00:00 2001 From: Sumin Date: Sun, 17 May 2026 15:33:11 +0900 Subject: [PATCH 27/43] =?UTF-8?q?test:=20ReservationControllerTest=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationControllerTest.java | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 src/test/java/roomescape/domain/reservation/ReservationControllerTest.java diff --git a/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java b/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java new file mode 100644 index 0000000000..907ed2e382 --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java @@ -0,0 +1,223 @@ +package roomescape.domain.reservation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import roomescape.domain.reservation.dto.CreateReservationRequest; +import roomescape.domain.reservation.dto.CreateReservationResponse; +import roomescape.domain.reservation.dto.CreateReservationResponse.ThemePayload; +import roomescape.domain.reservation.dto.UpdateReservationRequest; +import roomescape.domain.reservation.dto.UserReservationResponse; +import roomescape.domain.reservationdate.ReservationDate; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.Theme; +import roomescape.support.exception.NotFoundException; +import roomescape.support.exception.errors.ReservationErrors; + +@WebMvcTest(ReservationController.class) +class ReservationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ReservationService reservationService; + + @Test + @DisplayName("예약 생성 요청과 응답을 확인한다.") + void createReservation() throws Exception { + // given + CreateReservationRequest request = new CreateReservationRequest( + "보예", + 1L, + 2L, + 3L + ); + CreateReservationResponse response = new CreateReservationResponse( + 10L, + "보예", + LocalDate.of(2026, 5, 20), + LocalTime.of(10, 0), + ThemePayload.from(Theme.of(1L, "공포", "무섭다", "theme-url")) + ); + given(reservationService.createReservation(any(CreateReservationRequest.class))) + .willReturn(response); + + // when & then + mockMvc.perform(post("/reservations") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(10)) + .andExpect(jsonPath("$.name").value("보예")) + .andExpect(jsonPath("$.date").value("2026-05-20")) + .andExpect(jsonPath("$.time").value("10:00")) + .andExpect(jsonPath("$.theme.name").value("공포")) + .andExpect(jsonPath("$.theme.content").value("무섭다")) + .andExpect(jsonPath("$.theme.url").value("theme-url")); + } + + @Test + @DisplayName("필수 파라미터가 누락되었을 때 예외가 발생한다.") + void createWrongParameterReservation() throws Exception { + // given + CreateReservationRequest request = new CreateReservationRequest( + "보예", + 1L, + null, + 3L + ); + + // when & then + mockMvc.perform(post("/reservations") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INPUT_VALIDATION_ERROR")) + .andExpect(jsonPath("$.message").value("시간은 필수 선택 사항 입니다. 시간을 선택해주세요.")); + } + + @Test + @DisplayName("예약자 이름 조회의 요청과 응답을 확인한다.") + void getUserReservations() throws Exception { + // given + String name = "보예"; + UserReservationResponse response = UserReservationResponse.of("보예", + List.of(Reservation.of(1L, "보예", + ReservationDate.of(1L, LocalDate.of(2026, 5, 17)), + ReservationTime.of(1L, LocalTime.of(10, 10)), + Theme.of(1L, "공포", "아무서워", "theme-url") + ) + ) + ); + given(reservationService.getUserReservations(name)).willReturn(response); + + // when & then + mockMvc.perform(get("/reservations") + .contentType(MediaType.APPLICATION_JSON) + .param("name", name)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("보예")) + .andExpect(jsonPath("$.reservation[0].reservationId").value(1L)) + .andExpect(jsonPath("$.reservation[0].date.startWhen").value("2026-05-17")) + .andExpect(jsonPath("$.reservation[0].time.startAt").value("10:10")) + .andExpect(jsonPath("$.reservation[0].theme.name").value("공포")) + .andExpect(jsonPath("$.reservation[0].theme.content").value("아무서워")) + .andExpect(jsonPath("$.reservation[0].theme.url").value("theme-url")); + } + + @Test + @DisplayName("예약자 이름 조회 시 이름이 누락되면 예외가 발생한다.") + void getUserReservationsWithoutName() throws Exception { + // given & when & then + mockMvc.perform(get("/reservations") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("REQUIRED_PARAMETER_MISSING")) + .andExpect(jsonPath("$.message").value("필수 요청 파라미터가 누락되었습니다.")); + } + + @Test + @DisplayName("예약 삭제의 정상 요청과 응답을 확인한다.") + void deleteUserReservation() throws Exception { + // given + Long id = 1L; + + // when & then + mockMvc.perform(delete("/reservations/{id}", id)) + .andExpect(status().isNoContent()); + + verify(reservationService).cancelUserReservation(id); + } + + @Test + @DisplayName("예약 수정의 정상 요청과 응답을 확인한다.") + void updateReservation() throws Exception { + // given + Long id = 1L; + UpdateReservationRequest request = new UpdateReservationRequest( + LocalDate.of(2026, 5, 18), + LocalTime.of(14, 30) + ); + + // when & then + mockMvc.perform(patch("/reservations/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNoContent()); + + verify(reservationService).updateReservation(id, request); + } + + @Test + @DisplayName("예약 수정 시 잘못된 요청 형식이면 예외가 발생한다.") + void updateReservationWithInvalidFormat() throws Exception { + // given + Long id = 1L; + + // when & then + mockMvc.perform(patch("/reservations/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "startWhen": "2026/05/18", + "startAt": "14:30" + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INPUT_FORMAT_ERROR")) + .andExpect(jsonPath("$.message").value("입력 형식이 올바르지 않습니다. 날짜는 yyyy-MM-dd, 시간은 HH:mm 형식으로 입력해주세요.")); + } + + @Test + @DisplayName("수정할 예약이 없으면 예외가 발생한다.") + void updateReservationWhenReservationNotFound() throws Exception { + // given + Long id = 999L; + UpdateReservationRequest request = new UpdateReservationRequest( + LocalDate.of(2026, 5, 18), + LocalTime.of(14, 30) + ); + willThrow(new NotFoundException(ReservationErrors.RESERVATION_NOT_FOUND)) + .given(reservationService) + .updateReservation(id, request); + + // when & then + mockMvc.perform(patch("/reservations/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("RESERVATION_NOT_FOUND")) + .andExpect(jsonPath("$.message").value("존재하지 않는 예약건 입니다")); + } + + @Test + @DisplayName("예약 삭제 시 id를 누락한 경우 예외가 발생한다.") + void deleteUserReservationWithoutId() throws Exception { + // given & when & then + mockMvc.perform(delete("/reservations")) + .andExpect(status().isMethodNotAllowed()); + } +} From 88d4f62ce55132d9b8aa6e70aee5ab87c2a51aa2 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sun, 17 May 2026 16:12:48 +0900 Subject: [PATCH 28/43] =?UTF-8?q?test:=20AdminReservationControllerTest=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/AdminReservationControllerTest.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/test/java/roomescape/domain/reservation/admin/AdminReservationControllerTest.java diff --git a/src/test/java/roomescape/domain/reservation/admin/AdminReservationControllerTest.java b/src/test/java/roomescape/domain/reservation/admin/AdminReservationControllerTest.java new file mode 100644 index 0000000000..0fd1c738ed --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/admin/AdminReservationControllerTest.java @@ -0,0 +1,122 @@ +package roomescape.domain.reservation.admin; + + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import roomescape.domain.reservation.ReservationService; +import roomescape.domain.reservation.admin.dto.ReservationResponse; +import roomescape.domain.reservation.admin.dto.ReservationResponse.ReservationTimePayload; +import roomescape.domain.reservation.admin.dto.ReservationResponse.ThemePayload; +import roomescape.domain.reservationtime.ReservationTime; +import roomescape.domain.theme.Theme; +import roomescape.support.auth.AdminRequestValidator; + +@WebMvcTest(AdminReservationController.class) +class AdminReservationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ReservationService reservationService; + + @MockitoBean + private AdminRequestValidator validator; + + @Test + @DisplayName("관리자가 전체 예약 조회 시 요청과 응답을 확인한다.") + void getAllReservation() throws Exception { + // given + ReservationResponse response = new ReservationResponse( + 1L, + "보예", + LocalDate.of(2026, 5, 10), + ReservationTimePayload.from(ReservationTime.of(2L, LocalTime.of(10, 10))), + ThemePayload.from(Theme.of(3L, "공포", "으악 무서워!", "theme-url")) + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + given(reservationService.getAllReservations()) + .willReturn(List.of(response)); + + // when & then + mockMvc.perform(get("/admin/reservations") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("보예")) + .andExpect(jsonPath("$[0].date").value("2026-05-10")) + .andExpect(jsonPath("$[0].time.id").value(2)) + .andExpect(jsonPath("$[0].time.startAt").value("10:10")) + .andExpect(jsonPath("$[0].theme.id").value(3)) + .andExpect(jsonPath("$[0].theme.name").value("공포")) + .andExpect(jsonPath("$[0].theme.content").value("으악 무서워!")) + .andExpect(jsonPath("$[0].theme.url").value("theme-url")); + + verify(reservationService).getAllReservations(); + } + + @Test + @DisplayName("관리자 인증에 실패하면 전체 예약 조회 시 401을 반환한다.") + void getAllReservationWhenUnauthorized() throws Exception { + // given + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(get("/admin/reservations") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "wrong-token")) + .andExpect(status().isUnauthorized()); + + verify(reservationService, never()).getAllReservations(); + } + + @Test + @DisplayName("관리자가 예약 삭제 시 요청과 응답을 확인한다.") + void deleteReservation() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + + // when & then + mockMvc.perform(delete("/admin/reservations/{id}", id) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isNoContent()); + + verify(reservationService).cancelReservationByAdmin(id); + } + + @Test + @DisplayName("관리자 인증에 실패하면 예약 삭제 시 401을 반환한다.") + void deleteReservationWhenUnauthorized() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(delete("/admin/reservations/{id}", id) + .header("X-ADMIN-TOKEN", "wrong-token")) + .andExpect(status().isUnauthorized()); + + verify(reservationService, never()).cancelReservationByAdmin(id); + } +} From 3cb6be4d7bc4537eb4eacab3d038a98755ec1ff0 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sun, 17 May 2026 16:17:07 +0900 Subject: [PATCH 29/43] =?UTF-8?q?refactor:=20ReservationDate=20=EC=96=B4?= =?UTF-8?q?=EB=93=9C=EB=AF=BC=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationDateController.java | 45 -------------- .../ReservationDateService.java | 6 +- .../admin/AdminReservationDateController.java | 60 +++++++++++++++++++ .../dto/AdminReservationDateResponse.java | 2 +- .../dto/CreateReservationDateRequest.java | 2 +- .../dto/CreateReservationDateResponse.java | 2 +- .../ReservationDateServiceTest.java | 6 +- 7 files changed, 69 insertions(+), 54 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservationdate/admin/AdminReservationDateController.java rename src/main/java/roomescape/domain/reservationdate/{ => admin}/dto/AdminReservationDateResponse.java (87%) rename src/main/java/roomescape/domain/reservationdate/{ => admin}/dto/CreateReservationDateRequest.java (88%) rename src/main/java/roomescape/domain/reservationdate/{ => admin}/dto/CreateReservationDateResponse.java (87%) diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateController.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateController.java index ae287659f6..d4795f1cda 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateController.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateController.java @@ -1,62 +1,17 @@ package roomescape.domain.reservationdate; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.domain.reservationdate.dto.AdminReservationDateResponse; -import roomescape.domain.reservationdate.dto.CreateReservationDateRequest; -import roomescape.domain.reservationdate.dto.CreateReservationDateResponse; import roomescape.domain.reservationdate.dto.ReservationDateResponse; -import roomescape.support.auth.AdminRequestValidator; @RestController @RequiredArgsConstructor public class ReservationDateController { private final ReservationDateService reservationDateService; - private final AdminRequestValidator validator; - - @GetMapping("/admin/reservation-dates") - public ResponseEntity> getAllReservationDateForAdmin( - HttpServletRequest request - ) { - if (validator.isUnauthorized(request)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - List response = reservationDateService.getAllReservationDateForAdmin(); - return ResponseEntity.ok(response); - } - - @PostMapping("/admin/reservation-dates") - public ResponseEntity createReservationDate( - HttpServletRequest httpServletRequest, - @Valid @RequestBody CreateReservationDateRequest createReservationDateRequest - ) { - if (validator.isUnauthorized(httpServletRequest)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - CreateReservationDateResponse response = reservationDateService - .createReservationDate(createReservationDateRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - @DeleteMapping("/admin/reservation-dates/{id}") - public ResponseEntity deleteReservationDate(@PathVariable Long id, HttpServletRequest request) { - if (validator.isUnauthorized(request)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - reservationDateService.deleteReservationDate(id); - return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); - } @GetMapping("/reservation-dates") public ResponseEntity> getAllReservationDates() { diff --git a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java index 6bcbb45faf..491b00ba4a 100644 --- a/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java +++ b/src/main/java/roomescape/domain/reservationdate/ReservationDateService.java @@ -4,9 +4,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; -import roomescape.domain.reservationdate.dto.AdminReservationDateResponse; -import roomescape.domain.reservationdate.dto.CreateReservationDateRequest; -import roomescape.domain.reservationdate.dto.CreateReservationDateResponse; +import roomescape.domain.reservationdate.admin.dto.AdminReservationDateResponse; +import roomescape.domain.reservationdate.admin.dto.CreateReservationDateRequest; +import roomescape.domain.reservationdate.admin.dto.CreateReservationDateResponse; import roomescape.domain.reservationdate.dto.ReservationDateResponse; import roomescape.support.exception.ConflictException; import roomescape.support.exception.errors.ReservationDateErrors; diff --git a/src/main/java/roomescape/domain/reservationdate/admin/AdminReservationDateController.java b/src/main/java/roomescape/domain/reservationdate/admin/AdminReservationDateController.java new file mode 100644 index 0000000000..3aeeaf52c1 --- /dev/null +++ b/src/main/java/roomescape/domain/reservationdate/admin/AdminReservationDateController.java @@ -0,0 +1,60 @@ +package roomescape.domain.reservationdate.admin; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import roomescape.domain.reservationdate.ReservationDateService; +import roomescape.domain.reservationdate.admin.dto.AdminReservationDateResponse; +import roomescape.domain.reservationdate.admin.dto.CreateReservationDateRequest; +import roomescape.domain.reservationdate.admin.dto.CreateReservationDateResponse; +import roomescape.support.auth.AdminRequestValidator; + +@RestController +@RequiredArgsConstructor +public class AdminReservationDateController { + + private final ReservationDateService reservationDateService; + private final AdminRequestValidator validator; + + @GetMapping("/admin/reservation-dates") + public ResponseEntity> getAllReservationDateForAdmin( + HttpServletRequest request + ) { + if (validator.isUnauthorized(request)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + List response = reservationDateService.getAllReservationDateForAdmin(); + return ResponseEntity.ok(response); + } + + @PostMapping("/admin/reservation-dates") + public ResponseEntity createReservationDate( + HttpServletRequest httpServletRequest, + @Valid @RequestBody CreateReservationDateRequest createReservationDateRequest + ) { + if (validator.isUnauthorized(httpServletRequest)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + CreateReservationDateResponse response = reservationDateService + .createReservationDate(createReservationDateRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @DeleteMapping("/admin/reservation-dates/{id}") + public ResponseEntity deleteReservationDate(@PathVariable Long id, HttpServletRequest request) { + if (validator.isUnauthorized(request)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + reservationDateService.deleteReservationDate(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } +} diff --git a/src/main/java/roomescape/domain/reservationdate/dto/AdminReservationDateResponse.java b/src/main/java/roomescape/domain/reservationdate/admin/dto/AdminReservationDateResponse.java similarity index 87% rename from src/main/java/roomescape/domain/reservationdate/dto/AdminReservationDateResponse.java rename to src/main/java/roomescape/domain/reservationdate/admin/dto/AdminReservationDateResponse.java index 073ed78f55..7d51fe6e27 100644 --- a/src/main/java/roomescape/domain/reservationdate/dto/AdminReservationDateResponse.java +++ b/src/main/java/roomescape/domain/reservationdate/admin/dto/AdminReservationDateResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationdate.dto; +package roomescape.domain.reservationdate.admin.dto; import java.time.LocalDate; import roomescape.domain.reservationdate.ReservationDate; diff --git a/src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateRequest.java b/src/main/java/roomescape/domain/reservationdate/admin/dto/CreateReservationDateRequest.java similarity index 88% rename from src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateRequest.java rename to src/main/java/roomescape/domain/reservationdate/admin/dto/CreateReservationDateRequest.java index 666dce39c5..190d32c894 100644 --- a/src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateRequest.java +++ b/src/main/java/roomescape/domain/reservationdate/admin/dto/CreateReservationDateRequest.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationdate.dto; +package roomescape.domain.reservationdate.admin.dto; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; diff --git a/src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateResponse.java b/src/main/java/roomescape/domain/reservationdate/admin/dto/CreateReservationDateResponse.java similarity index 87% rename from src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateResponse.java rename to src/main/java/roomescape/domain/reservationdate/admin/dto/CreateReservationDateResponse.java index 727f589b45..d13bddfd9e 100644 --- a/src/main/java/roomescape/domain/reservationdate/dto/CreateReservationDateResponse.java +++ b/src/main/java/roomescape/domain/reservationdate/admin/dto/CreateReservationDateResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationdate.dto; +package roomescape.domain.reservationdate.admin.dto; import java.time.LocalDate; import roomescape.domain.reservationdate.ReservationDate; diff --git a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java index a239e7696a..fcc612e197 100644 --- a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java +++ b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java @@ -10,9 +10,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import roomescape.domain.reservation.Reservation; -import roomescape.domain.reservationdate.dto.AdminReservationDateResponse; -import roomescape.domain.reservationdate.dto.CreateReservationDateRequest; -import roomescape.domain.reservationdate.dto.CreateReservationDateResponse; +import roomescape.domain.reservationdate.admin.dto.AdminReservationDateResponse; +import roomescape.domain.reservationdate.admin.dto.CreateReservationDateRequest; +import roomescape.domain.reservationdate.admin.dto.CreateReservationDateResponse; import roomescape.domain.reservationtime.ReservationTime; import roomescape.domain.theme.Theme; import roomescape.support.exception.RoomescapeException; From d823f2d5499b080ddde8d6963a103769d7af666c Mon Sep 17 00:00:00 2001 From: Sumin Date: Sun, 17 May 2026 16:33:15 +0900 Subject: [PATCH 30/43] =?UTF-8?q?test:=20ReservationDate=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationDateControllerTest.java | 49 +++++ .../AdminReservationDateControllerTest.java | 201 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 src/test/java/roomescape/domain/reservationdate/ReservationDateControllerTest.java create mode 100644 src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateControllerTest.java diff --git a/src/test/java/roomescape/domain/reservationdate/ReservationDateControllerTest.java b/src/test/java/roomescape/domain/reservationdate/ReservationDateControllerTest.java new file mode 100644 index 0000000000..d44bcfe030 --- /dev/null +++ b/src/test/java/roomescape/domain/reservationdate/ReservationDateControllerTest.java @@ -0,0 +1,49 @@ +package roomescape.domain.reservationdate; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import roomescape.domain.reservationdate.dto.ReservationDateResponse; + +@WebMvcTest(ReservationDateController.class) +class ReservationDateControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ReservationDateService reservationDateService; + + @Test + @DisplayName("예약 날짜 조회의 요청과 응답을 확인한다.") + void getAllReservationDates() throws Exception { + // given + ReservationDateResponse response = new ReservationDateResponse( + 1L, + LocalDate.of(2026, 5, 20) + ); + given(reservationDateService.getAllReservationDate()) + .willReturn(List.of(response)); + + // when & then + mockMvc.perform(get("/reservation-dates") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].reservationDate").value("2026-05-20")); + + verify(reservationDateService).getAllReservationDate(); + } +} diff --git a/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateControllerTest.java b/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateControllerTest.java new file mode 100644 index 0000000000..a03445b4f0 --- /dev/null +++ b/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateControllerTest.java @@ -0,0 +1,201 @@ +package roomescape.domain.reservationdate.admin; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import roomescape.domain.reservationdate.ReservationDateService; +import roomescape.domain.reservationdate.admin.dto.AdminReservationDateResponse; +import roomescape.domain.reservationdate.admin.dto.CreateReservationDateRequest; +import roomescape.domain.reservationdate.admin.dto.CreateReservationDateResponse; +import roomescape.support.auth.AdminRequestValidator; +import roomescape.support.exception.ConflictException; +import roomescape.support.exception.errors.ReservationDateErrors; + +@WebMvcTest(AdminReservationDateController.class) +class AdminReservationDateControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ReservationDateService reservationDateService; + + @MockitoBean + private AdminRequestValidator validator; + + @Test + @DisplayName("관리자가 전체 예약 날짜 조회 시 요청과 응답을 확인한다.") + void getAllReservationDatesForAdmin() throws Exception { + // given + AdminReservationDateResponse response = new AdminReservationDateResponse( + 1L, + LocalDate.of(2026, 5, 20) + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + given(reservationDateService.getAllReservationDateForAdmin()) + .willReturn(List.of(response)); + + // when & then + mockMvc.perform(get("/admin/reservation-dates") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].reservationDate").value("2026-05-20")); + + verify(reservationDateService).getAllReservationDateForAdmin(); + } + + @Test + @DisplayName("관리자 인증에 실패하면 전체 예약 날짜 조회 시 401을 반환한다.") + void getAllReservationDatesForAdminWhenUnauthorized() throws Exception { + // given + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(get("/admin/reservation-dates") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "wrong-token")) + .andExpect(status().isUnauthorized()); + + verify(reservationDateService, never()).getAllReservationDateForAdmin(); + } + + @Test + @DisplayName("관리자가 예약 날짜 생성 시 요청과 응답을 확인한다.") + void createReservationDate() throws Exception { + // given + CreateReservationDateRequest request = new CreateReservationDateRequest( + LocalDate.of(2026, 5, 20) + ); + CreateReservationDateResponse response = new CreateReservationDateResponse( + 1L, + LocalDate.of(2026, 5, 20) + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + given(reservationDateService.createReservationDate(any(CreateReservationDateRequest.class))) + .willReturn(response); + + // when & then + mockMvc.perform(post("/admin/reservation-dates") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.reservationDate").value("2026-05-20")); + + verify(reservationDateService).createReservationDate(request); + } + + @Test + @DisplayName("관리자 인증에 실패하면 예약 날짜 생성 시 401을 반환한다.") + void createReservationDateWhenUnauthorized() throws Exception { + // given + CreateReservationDateRequest request = new CreateReservationDateRequest( + LocalDate.of(2026, 5, 20) + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(post("/admin/reservation-dates") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "wrong-token") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + + verify(reservationDateService, never()).createReservationDate(any(CreateReservationDateRequest.class)); + } + + @Test + @DisplayName("예약 날짜 생성 시 필수 요청값이 누락되면 예외가 발생한다.") + void createReservationDateWithoutReservationDate() throws Exception { + // given + String request = """ + { + "reservationDate": null + } + """; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + + // when & then + mockMvc.perform(post("/admin/reservation-dates") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token") + .content(request)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INPUT_VALIDATION_ERROR")) + .andExpect(jsonPath("$.message").value("예약 날짜는 필수 사항 입니다. 날짜를 선택해주세요.")); + } + + @Test + @DisplayName("관리자가 예약 날짜 삭제 시 요청과 응답을 확인한다.") + void deleteReservationDate() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + + // when & then + mockMvc.perform(delete("/admin/reservation-dates/{id}", id) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isNoContent()); + + verify(reservationDateService).deleteReservationDate(id); + } + + @Test + @DisplayName("관리자 인증에 실패하면 예약 날짜 삭제 시 401을 반환한다.") + void deleteReservationDateWhenUnauthorized() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(delete("/admin/reservation-dates/{id}", id) + .header("X-ADMIN-TOKEN", "wrong-token")) + .andExpect(status().isUnauthorized()); + + verify(reservationDateService, never()).deleteReservationDate(id); + } + + @Test + @DisplayName("이미 예약이 존재하는 날짜는 삭제할 수 없다.") + void deleteReservationDateWhenDateInUse() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + willThrow(new ConflictException(ReservationDateErrors.RESERVATION_DATE_IN_USE)) + .given(reservationDateService) + .deleteReservationDate(id); + + // when & then + mockMvc.perform(delete("/admin/reservation-dates/{id}", id) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("RESERVATION_DATE_IN_USE")) + .andExpect(jsonPath("$.message").value("이미 예약이 존재하는 날짜는 삭제할 수 없습니다.")); + } +} From 0bf7829012837ce9d5e44224af31225213b64332 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sun, 17 May 2026 16:41:16 +0900 Subject: [PATCH 31/43] =?UTF-8?q?refactor:=20ReservationTime=20=EC=96=B4?= =?UTF-8?q?=EB=93=9C=EB=AF=BC=20=EA=B8=B0=EB=8A=A5=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationTimeController.java | 42 --------------- .../ReservationTimeService.java | 6 +-- .../admin/AdminReservationTimeController.java | 54 +++++++++++++++++++ .../{dto => admin}/CreateTimeRequest.java | 2 +- .../{dto => admin}/CreateTimeResponse.java | 2 +- .../ReservationTimeResponse.java | 2 +- .../ReservationTimeServiceTest.java | 6 +-- 7 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 src/main/java/roomescape/domain/reservationtime/admin/AdminReservationTimeController.java rename src/main/java/roomescape/domain/reservationtime/{dto => admin}/CreateTimeRequest.java (88%) rename src/main/java/roomescape/domain/reservationtime/{dto => admin}/CreateTimeResponse.java (90%) rename src/main/java/roomescape/domain/reservationtime/{dto => admin}/ReservationTimeResponse.java (90%) diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java index 2ef9d6a560..2cfd182c47 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeController.java @@ -1,60 +1,18 @@ package roomescape.domain.reservationtime; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import roomescape.domain.reservationtime.dto.CreateTimeRequest; -import roomescape.domain.reservationtime.dto.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; -import roomescape.domain.reservationtime.dto.ReservationTimeResponse; -import roomescape.support.auth.AdminRequestValidator; @RestController @RequiredArgsConstructor public class ReservationTimeController { private final ReservationTimeService reservationTimeService; - private final AdminRequestValidator validator; - - @GetMapping("/admin/times") - public ResponseEntity> getAllReservationTime(HttpServletRequest request) { - if (validator.isUnauthorized(request)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - List response = reservationTimeService.getAllReservationTime(); - return ResponseEntity.ok(response); - } - - @PostMapping("/admin/times") - public ResponseEntity createReservationTime( - HttpServletRequest httpServletRequest, - @Valid @RequestBody CreateTimeRequest createTimeRequest - ) { - if (validator.isUnauthorized(httpServletRequest)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - CreateTimeResponse response = reservationTimeService.createReservationTime(createTimeRequest); - return ResponseEntity.ok(response); - } - - @DeleteMapping("/admin/times/{id}") - public ResponseEntity deleteReservationTime(HttpServletRequest request, @PathVariable Long id) { - if (validator.isUnauthorized(request)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - reservationTimeService.deleteReservationTime(id); - return ResponseEntity.ok().build(); - } @GetMapping("/reservation-times/availability") public ResponseEntity> getReservationTimeAvailability( diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java index 919d07ea9b..948c0c3c1f 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java @@ -7,10 +7,10 @@ import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.ReservationDateRepository; -import roomescape.domain.reservationtime.dto.CreateTimeRequest; -import roomescape.domain.reservationtime.dto.CreateTimeResponse; +import roomescape.domain.reservationtime.admin.CreateTimeRequest; +import roomescape.domain.reservationtime.admin.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; -import roomescape.domain.reservationtime.dto.ReservationTimeResponse; +import roomescape.domain.reservationtime.admin.ReservationTimeResponse; import roomescape.domain.theme.ThemeRepository; import roomescape.support.exception.ConflictException; import roomescape.support.exception.NotFoundException; diff --git a/src/main/java/roomescape/domain/reservationtime/admin/AdminReservationTimeController.java b/src/main/java/roomescape/domain/reservationtime/admin/AdminReservationTimeController.java new file mode 100644 index 0000000000..2dcc4e481c --- /dev/null +++ b/src/main/java/roomescape/domain/reservationtime/admin/AdminReservationTimeController.java @@ -0,0 +1,54 @@ +package roomescape.domain.reservationtime.admin; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import roomescape.domain.reservationtime.ReservationTimeService; +import roomescape.support.auth.AdminRequestValidator; + +@RestController +@RequiredArgsConstructor +public class AdminReservationTimeController { + + private final ReservationTimeService reservationTimeService; + private final AdminRequestValidator validator; + + @GetMapping("/admin/times") + public ResponseEntity> getAllReservationTime(HttpServletRequest request) { + if (validator.isUnauthorized(request)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + List response = reservationTimeService.getAllReservationTime(); + return ResponseEntity.ok(response); + } + + @PostMapping("/admin/times") + public ResponseEntity createReservationTime( + HttpServletRequest httpServletRequest, + @Valid @RequestBody CreateTimeRequest createTimeRequest + ) { + if (validator.isUnauthorized(httpServletRequest)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + CreateTimeResponse response = reservationTimeService.createReservationTime(createTimeRequest); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/admin/times/{id}") + public ResponseEntity deleteReservationTime(HttpServletRequest request, @PathVariable Long id) { + if (validator.isUnauthorized(request)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + reservationTimeService.deleteReservationTime(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/roomescape/domain/reservationtime/dto/CreateTimeRequest.java b/src/main/java/roomescape/domain/reservationtime/admin/CreateTimeRequest.java similarity index 88% rename from src/main/java/roomescape/domain/reservationtime/dto/CreateTimeRequest.java rename to src/main/java/roomescape/domain/reservationtime/admin/CreateTimeRequest.java index 04384fe625..2573e82556 100644 --- a/src/main/java/roomescape/domain/reservationtime/dto/CreateTimeRequest.java +++ b/src/main/java/roomescape/domain/reservationtime/admin/CreateTimeRequest.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationtime.dto; +package roomescape.domain.reservationtime.admin; import jakarta.validation.constraints.NotNull; import java.time.LocalTime; diff --git a/src/main/java/roomescape/domain/reservationtime/dto/CreateTimeResponse.java b/src/main/java/roomescape/domain/reservationtime/admin/CreateTimeResponse.java similarity index 90% rename from src/main/java/roomescape/domain/reservationtime/dto/CreateTimeResponse.java rename to src/main/java/roomescape/domain/reservationtime/admin/CreateTimeResponse.java index 295bb967f4..647db0f0ee 100644 --- a/src/main/java/roomescape/domain/reservationtime/dto/CreateTimeResponse.java +++ b/src/main/java/roomescape/domain/reservationtime/admin/CreateTimeResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationtime.dto; +package roomescape.domain.reservationtime.admin; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalTime; diff --git a/src/main/java/roomescape/domain/reservationtime/dto/ReservationTimeResponse.java b/src/main/java/roomescape/domain/reservationtime/admin/ReservationTimeResponse.java similarity index 90% rename from src/main/java/roomescape/domain/reservationtime/dto/ReservationTimeResponse.java rename to src/main/java/roomescape/domain/reservationtime/admin/ReservationTimeResponse.java index d1c9e96357..aa4fc9f994 100644 --- a/src/main/java/roomescape/domain/reservationtime/dto/ReservationTimeResponse.java +++ b/src/main/java/roomescape/domain/reservationtime/admin/ReservationTimeResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationtime.dto; +package roomescape.domain.reservationtime.admin; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalTime; diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java index 4f09c79703..d9e136d443 100644 --- a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java @@ -12,10 +12,10 @@ import org.junit.jupiter.api.Test; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationtime.dto.CreateTimeRequest; -import roomescape.domain.reservationtime.dto.CreateTimeResponse; +import roomescape.domain.reservationtime.admin.CreateTimeRequest; +import roomescape.domain.reservationtime.admin.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; -import roomescape.domain.reservationtime.dto.ReservationTimeResponse; +import roomescape.domain.reservationtime.admin.ReservationTimeResponse; import roomescape.domain.theme.Theme; import roomescape.support.exception.NotFoundException; import roomescape.support.exception.RoomescapeException; From 45a969566be30b800905758e9fbd60adaff677f4 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sun, 17 May 2026 16:46:10 +0900 Subject: [PATCH 32/43] =?UTF-8?q?test:=20ReservationTime=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReservationTimeControllerTest.java | 88 ++++++++ .../AdminReservationTimeControllerTest.java | 198 ++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 src/test/java/roomescape/domain/reservationtime/ReservationTimeControllerTest.java create mode 100644 src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeControllerTest.java diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeControllerTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeControllerTest.java new file mode 100644 index 0000000000..428212f4a2 --- /dev/null +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeControllerTest.java @@ -0,0 +1,88 @@ +package roomescape.domain.reservationtime; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; +import roomescape.support.exception.NotFoundException; +import roomescape.support.exception.errors.ThemeErrors; + +@WebMvcTest(ReservationTimeController.class) +class ReservationTimeControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ReservationTimeService reservationTimeService; + + @Test + @DisplayName("예약 가능 시간 조회의 요청과 응답을 확인한다.") + void getReservationTimeAvailability() throws Exception { + // given + Long themeId = 1L; + Long dateId = 2L; + ReservationTimeAvailabilityResponse response = new ReservationTimeAvailabilityResponse( + 3L, + LocalTime.of(10, 10), + true + ); + given(reservationTimeService.getReservationTimeAvailability(themeId, dateId)) + .willReturn(List.of(response)); + + // when & then + mockMvc.perform(get("/reservation-times/availability") + .contentType(MediaType.APPLICATION_JSON) + .param("themeId", String.valueOf(themeId)) + .param("dateId", String.valueOf(dateId))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].timeId").value(3)) + .andExpect(jsonPath("$[0].startAt").value("10:10")) + .andExpect(jsonPath("$[0].available").value(true)); + + verify(reservationTimeService).getReservationTimeAvailability(themeId, dateId); + } + + @Test + @DisplayName("예약 가능 시간 조회 시 themeId가 누락되면 예외가 발생한다.") + void getReservationTimeAvailabilityWithoutThemeId() throws Exception { + // given & when & then + mockMvc.perform(get("/reservation-times/availability") + .contentType(MediaType.APPLICATION_JSON) + .param("dateId", "2")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("REQUIRED_PARAMETER_MISSING")) + .andExpect(jsonPath("$.message").value("필수 요청 파라미터가 누락되었습니다.")); + } + + @Test + @DisplayName("존재하지 않는 테마로 예약 가능 시간 조회 시 예외가 발생한다.") + void getReservationTimeAvailabilityWhenThemeNotFound() throws Exception { + // given + Long themeId = 999L; + Long dateId = 2L; + given(reservationTimeService.getReservationTimeAvailability(themeId, dateId)) + .willThrow(new NotFoundException(ThemeErrors.THEME_NOT_EXIST)); + + // when & then + mockMvc.perform(get("/reservation-times/availability") + .contentType(MediaType.APPLICATION_JSON) + .param("themeId", String.valueOf(themeId)) + .param("dateId", String.valueOf(dateId))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("THEME_NOT_EXIST")) + .andExpect(jsonPath("$.message").value("존재하지 않는 테마 입니다.")); + } +} diff --git a/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeControllerTest.java b/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeControllerTest.java new file mode 100644 index 0000000000..5d0cf6ce64 --- /dev/null +++ b/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeControllerTest.java @@ -0,0 +1,198 @@ +package roomescape.domain.reservationtime.admin; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import roomescape.domain.reservationtime.ReservationTimeService; +import roomescape.support.auth.AdminRequestValidator; +import roomescape.support.exception.ConflictException; +import roomescape.support.exception.errors.ReservationTimeErrors; + +@WebMvcTest(AdminReservationTimeController.class) +class AdminReservationTimeControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ReservationTimeService reservationTimeService; + + @MockitoBean + private AdminRequestValidator validator; + + @Test + @DisplayName("관리자가 전체 예약 시간 조회 시 요청과 응답을 확인한다.") + void getAllReservationTime() throws Exception { + // given + ReservationTimeResponse response = new ReservationTimeResponse( + 1L, + LocalTime.of(10, 10) + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + given(reservationTimeService.getAllReservationTime()) + .willReturn(List.of(response)); + + // when & then + mockMvc.perform(get("/admin/times") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].startAt").value("10:10")); + + verify(reservationTimeService).getAllReservationTime(); + } + + @Test + @DisplayName("관리자 인증에 실패하면 전체 예약 시간 조회 시 401을 반환한다.") + void getAllReservationTimeWhenUnauthorized() throws Exception { + // given + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(get("/admin/times") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "wrong-token")) + .andExpect(status().isUnauthorized()); + + verify(reservationTimeService, never()).getAllReservationTime(); + } + + @Test + @DisplayName("관리자가 예약 시간 생성 시 요청과 응답을 확인한다.") + void createReservationTime() throws Exception { + // given + CreateTimeRequest request = new CreateTimeRequest( + LocalTime.of(10, 10) + ); + CreateTimeResponse response = new CreateTimeResponse( + 1L, + LocalTime.of(10, 10) + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + given(reservationTimeService.createReservationTime(any(CreateTimeRequest.class))) + .willReturn(response); + + // when & then + mockMvc.perform(post("/admin/times") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.startAt").value("10:10")); + + verify(reservationTimeService).createReservationTime(request); + } + + @Test + @DisplayName("관리자 인증에 실패하면 예약 시간 생성 시 401을 반환한다.") + void createReservationTimeWhenUnauthorized() throws Exception { + // given + CreateTimeRequest request = new CreateTimeRequest( + LocalTime.of(10, 10) + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(post("/admin/times") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "wrong-token") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + + verify(reservationTimeService, never()).createReservationTime(any(CreateTimeRequest.class)); + } + + @Test + @DisplayName("예약 시간 생성 시 필수 요청값이 누락되면 예외가 발생한다.") + void createReservationTimeWithoutStartAt() throws Exception { + // given + String request = """ + { + "startAt": null + } + """; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + + // when & then + mockMvc.perform(post("/admin/times") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token") + .content(request)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INPUT_VALIDATION_ERROR")) + .andExpect(jsonPath("$.message").value("시간은 필수 사항 입니다. 시간을 선택해주세요.")); + } + + @Test + @DisplayName("관리자가 예약 시간 삭제 시 요청과 응답을 확인한다.") + void deleteReservationTime() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + + // when & then + mockMvc.perform(delete("/admin/times/{id}", id) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isOk()); + + verify(reservationTimeService).deleteReservationTime(id); + } + + @Test + @DisplayName("관리자 인증에 실패하면 예약 시간 삭제 시 401을 반환한다.") + void deleteReservationTimeWhenUnauthorized() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(delete("/admin/times/{id}", id) + .header("X-ADMIN-TOKEN", "wrong-token")) + .andExpect(status().isUnauthorized()); + + verify(reservationTimeService, never()).deleteReservationTime(id); + } + + @Test + @DisplayName("이미 예약이 존재하는 시간대는 삭제할 수 없다.") + void deleteReservationTimeWhenTimeInUse() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + willThrow(new ConflictException(ReservationTimeErrors.RESERVATION_TIME_IN_USE)) + .given(reservationTimeService) + .deleteReservationTime(id); + + // when & then + mockMvc.perform(delete("/admin/times/{id}", id) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("RESERVATION_TIME_IN_USE")) + .andExpect(jsonPath("$.message").value("이미 예약이 존재하는 시간대는 삭제할 수 없습니다.")); + } +} From 4fe8125047925f504746a1b71932f70957e7c8db Mon Sep 17 00:00:00 2001 From: Sumin Date: Sun, 17 May 2026 16:48:55 +0900 Subject: [PATCH 33/43] =?UTF-8?q?refactor:=20Theme=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/theme/ThemeController.java | 42 -------------- .../domain/theme/ThemeRepository.java | 2 +- .../roomescape/domain/theme/ThemeService.java | 6 +- .../theme/admin/AdminThemeController.java | 57 +++++++++++++++++++ .../{ => admin}/dto/AdminThemeResponse.java | 2 +- .../{ => admin}/dto/CreateThemeRequest.java | 2 +- .../{ => admin}/dto/CreateThemeResponse.java | 2 +- .../domain/theme/ThemeServiceTest.java | 6 +- 8 files changed, 67 insertions(+), 52 deletions(-) create mode 100644 src/main/java/roomescape/domain/theme/admin/AdminThemeController.java rename src/main/java/roomescape/domain/theme/{ => admin}/dto/AdminThemeResponse.java (89%) rename src/main/java/roomescape/domain/theme/{ => admin}/dto/CreateThemeRequest.java (92%) rename src/main/java/roomescape/domain/theme/{ => admin}/dto/CreateThemeResponse.java (89%) diff --git a/src/main/java/roomescape/domain/theme/ThemeController.java b/src/main/java/roomescape/domain/theme/ThemeController.java index 19a22017a3..9afebccbb0 100644 --- a/src/main/java/roomescape/domain/theme/ThemeController.java +++ b/src/main/java/roomescape/domain/theme/ThemeController.java @@ -1,60 +1,18 @@ package roomescape.domain.theme; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.domain.theme.dto.AdminThemeResponse; -import roomescape.domain.theme.dto.CreateThemeRequest; -import roomescape.domain.theme.dto.CreateThemeResponse; import roomescape.domain.theme.dto.ThemeRankResponse; import roomescape.domain.theme.dto.ThemeResponse; -import roomescape.support.auth.AdminRequestValidator; @RestController @RequiredArgsConstructor public class ThemeController { private final ThemeService themeService; - private final AdminRequestValidator validator; - - @GetMapping("/admin/themes") - public ResponseEntity> getAllThemeForAdmin(HttpServletRequest request) { - if (validator.isUnauthorized(request)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - List response = themeService.getAllThemeForAdmin(); - return ResponseEntity.ok(response); - } - - @PostMapping("/admin/themes") - public ResponseEntity createTheme( - @Valid @RequestBody CreateThemeRequest createThemeRequest, - HttpServletRequest httpServletRequest - ) { - if (validator.isUnauthorized(httpServletRequest)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - CreateThemeResponse response = themeService.createTheme(createThemeRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - @DeleteMapping("/admin/themes/{id}") - public ResponseEntity deleteTheme(@PathVariable Long id, HttpServletRequest request) { - if (validator.isUnauthorized(request)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - themeService.deleteTheme(id); - return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); - } @GetMapping("/themes") public ResponseEntity> getAllTheme() { diff --git a/src/main/java/roomescape/domain/theme/ThemeRepository.java b/src/main/java/roomescape/domain/theme/ThemeRepository.java index f92dcd8b2e..ba171e7dff 100644 --- a/src/main/java/roomescape/domain/theme/ThemeRepository.java +++ b/src/main/java/roomescape/domain/theme/ThemeRepository.java @@ -1,7 +1,7 @@ package roomescape.domain.theme; -import java.util.Optional; import java.util.List; +import java.util.Optional; public interface ThemeRepository { diff --git a/src/main/java/roomescape/domain/theme/ThemeService.java b/src/main/java/roomescape/domain/theme/ThemeService.java index 15a279f8ea..5aba2913e2 100644 --- a/src/main/java/roomescape/domain/theme/ThemeService.java +++ b/src/main/java/roomescape/domain/theme/ThemeService.java @@ -5,9 +5,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; -import roomescape.domain.theme.dto.AdminThemeResponse; -import roomescape.domain.theme.dto.CreateThemeRequest; -import roomescape.domain.theme.dto.CreateThemeResponse; +import roomescape.domain.theme.admin.dto.AdminThemeResponse; +import roomescape.domain.theme.admin.dto.CreateThemeRequest; +import roomescape.domain.theme.admin.dto.CreateThemeResponse; import roomescape.domain.theme.dto.ThemeRankResponse; import roomescape.domain.theme.dto.ThemeResponse; import roomescape.support.exception.ConflictException; diff --git a/src/main/java/roomescape/domain/theme/admin/AdminThemeController.java b/src/main/java/roomescape/domain/theme/admin/AdminThemeController.java new file mode 100644 index 0000000000..4a7b4657c2 --- /dev/null +++ b/src/main/java/roomescape/domain/theme/admin/AdminThemeController.java @@ -0,0 +1,57 @@ +package roomescape.domain.theme.admin; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import roomescape.domain.theme.ThemeService; +import roomescape.domain.theme.admin.dto.AdminThemeResponse; +import roomescape.domain.theme.admin.dto.CreateThemeRequest; +import roomescape.domain.theme.admin.dto.CreateThemeResponse; +import roomescape.support.auth.AdminRequestValidator; + +@RestController +@RequiredArgsConstructor +public class AdminThemeController { + + private final ThemeService themeService; + private final AdminRequestValidator validator; + + @GetMapping("/admin/themes") + public ResponseEntity> getAllThemeForAdmin(HttpServletRequest request) { + if (validator.isUnauthorized(request)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + List response = themeService.getAllThemeForAdmin(); + return ResponseEntity.ok(response); + } + + @PostMapping("/admin/themes") + public ResponseEntity createTheme( + @Valid @RequestBody CreateThemeRequest createThemeRequest, + HttpServletRequest httpServletRequest + ) { + if (validator.isUnauthorized(httpServletRequest)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + CreateThemeResponse response = themeService.createTheme(createThemeRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @DeleteMapping("/admin/themes/{id}") + public ResponseEntity deleteTheme(@PathVariable Long id, HttpServletRequest request) { + if (validator.isUnauthorized(request)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + themeService.deleteTheme(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } +} diff --git a/src/main/java/roomescape/domain/theme/dto/AdminThemeResponse.java b/src/main/java/roomescape/domain/theme/admin/dto/AdminThemeResponse.java similarity index 89% rename from src/main/java/roomescape/domain/theme/dto/AdminThemeResponse.java rename to src/main/java/roomescape/domain/theme/admin/dto/AdminThemeResponse.java index df52064a97..5d83efa74d 100644 --- a/src/main/java/roomescape/domain/theme/dto/AdminThemeResponse.java +++ b/src/main/java/roomescape/domain/theme/admin/dto/AdminThemeResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.theme.dto; +package roomescape.domain.theme.admin.dto; import roomescape.domain.theme.Theme; diff --git a/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java b/src/main/java/roomescape/domain/theme/admin/dto/CreateThemeRequest.java similarity index 92% rename from src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java rename to src/main/java/roomescape/domain/theme/admin/dto/CreateThemeRequest.java index 421b98b983..e0b36b616e 100644 --- a/src/main/java/roomescape/domain/theme/dto/CreateThemeRequest.java +++ b/src/main/java/roomescape/domain/theme/admin/dto/CreateThemeRequest.java @@ -1,4 +1,4 @@ -package roomescape.domain.theme.dto; +package roomescape.domain.theme.admin.dto; import jakarta.validation.constraints.NotBlank; import roomescape.domain.theme.Theme; diff --git a/src/main/java/roomescape/domain/theme/dto/CreateThemeResponse.java b/src/main/java/roomescape/domain/theme/admin/dto/CreateThemeResponse.java similarity index 89% rename from src/main/java/roomescape/domain/theme/dto/CreateThemeResponse.java rename to src/main/java/roomescape/domain/theme/admin/dto/CreateThemeResponse.java index 3a40c0e6aa..0da8af07aa 100644 --- a/src/main/java/roomescape/domain/theme/dto/CreateThemeResponse.java +++ b/src/main/java/roomescape/domain/theme/admin/dto/CreateThemeResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.theme.dto; +package roomescape.domain.theme.admin.dto; import roomescape.domain.theme.Theme; diff --git a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java index 520ef0c80f..d38807f1d3 100644 --- a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java @@ -6,9 +6,9 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import roomescape.domain.theme.dto.AdminThemeResponse; -import roomescape.domain.theme.dto.CreateThemeRequest; -import roomescape.domain.theme.dto.CreateThemeResponse; +import roomescape.domain.theme.admin.dto.AdminThemeResponse; +import roomescape.domain.theme.admin.dto.CreateThemeRequest; +import roomescape.domain.theme.admin.dto.CreateThemeResponse; import roomescape.domain.theme.dto.ThemeResponse; import roomescape.support.fake.FakeReservationRepository; import roomescape.support.fake.FakeThemeRepository; From c574b71c10b7d0605f5647915763f3e1e8a2b7a8 Mon Sep 17 00:00:00 2001 From: Sumin Date: Sun, 17 May 2026 16:49:29 +0900 Subject: [PATCH 34/43] =?UTF-8?q?refactor:=20ReservationTime=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reservationtime/ReservationTimeService.java | 6 +++--- .../admin/AdminReservationTimeController.java | 3 +++ .../reservationtime/admin/{ => dto}/CreateTimeRequest.java | 2 +- .../reservationtime/admin/{ => dto}/CreateTimeResponse.java | 2 +- .../admin/{ => dto}/ReservationTimeResponse.java | 2 +- .../domain/reservationtime/ReservationTimeServiceTest.java | 6 +++--- .../admin/AdminReservationTimeControllerTest.java | 3 +++ 7 files changed, 15 insertions(+), 9 deletions(-) rename src/main/java/roomescape/domain/reservationtime/admin/{ => dto}/CreateTimeRequest.java (88%) rename src/main/java/roomescape/domain/reservationtime/admin/{ => dto}/CreateTimeResponse.java (89%) rename src/main/java/roomescape/domain/reservationtime/admin/{ => dto}/ReservationTimeResponse.java (90%) diff --git a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java index 948c0c3c1f..8ed2783ba7 100644 --- a/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java +++ b/src/main/java/roomescape/domain/reservationtime/ReservationTimeService.java @@ -7,10 +7,10 @@ import org.springframework.stereotype.Service; import roomescape.domain.reservation.ReservationRepository; import roomescape.domain.reservationdate.ReservationDateRepository; -import roomescape.domain.reservationtime.admin.CreateTimeRequest; -import roomescape.domain.reservationtime.admin.CreateTimeResponse; +import roomescape.domain.reservationtime.admin.dto.CreateTimeRequest; +import roomescape.domain.reservationtime.admin.dto.CreateTimeResponse; +import roomescape.domain.reservationtime.admin.dto.ReservationTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; -import roomescape.domain.reservationtime.admin.ReservationTimeResponse; import roomescape.domain.theme.ThemeRepository; import roomescape.support.exception.ConflictException; import roomescape.support.exception.NotFoundException; diff --git a/src/main/java/roomescape/domain/reservationtime/admin/AdminReservationTimeController.java b/src/main/java/roomescape/domain/reservationtime/admin/AdminReservationTimeController.java index 2dcc4e481c..1a3f9d1ab3 100644 --- a/src/main/java/roomescape/domain/reservationtime/admin/AdminReservationTimeController.java +++ b/src/main/java/roomescape/domain/reservationtime/admin/AdminReservationTimeController.java @@ -13,6 +13,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import roomescape.domain.reservationtime.ReservationTimeService; +import roomescape.domain.reservationtime.admin.dto.CreateTimeRequest; +import roomescape.domain.reservationtime.admin.dto.CreateTimeResponse; +import roomescape.domain.reservationtime.admin.dto.ReservationTimeResponse; import roomescape.support.auth.AdminRequestValidator; @RestController diff --git a/src/main/java/roomescape/domain/reservationtime/admin/CreateTimeRequest.java b/src/main/java/roomescape/domain/reservationtime/admin/dto/CreateTimeRequest.java similarity index 88% rename from src/main/java/roomescape/domain/reservationtime/admin/CreateTimeRequest.java rename to src/main/java/roomescape/domain/reservationtime/admin/dto/CreateTimeRequest.java index 2573e82556..1322cca3aa 100644 --- a/src/main/java/roomescape/domain/reservationtime/admin/CreateTimeRequest.java +++ b/src/main/java/roomescape/domain/reservationtime/admin/dto/CreateTimeRequest.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationtime.admin; +package roomescape.domain.reservationtime.admin.dto; import jakarta.validation.constraints.NotNull; import java.time.LocalTime; diff --git a/src/main/java/roomescape/domain/reservationtime/admin/CreateTimeResponse.java b/src/main/java/roomescape/domain/reservationtime/admin/dto/CreateTimeResponse.java similarity index 89% rename from src/main/java/roomescape/domain/reservationtime/admin/CreateTimeResponse.java rename to src/main/java/roomescape/domain/reservationtime/admin/dto/CreateTimeResponse.java index 647db0f0ee..df406f6356 100644 --- a/src/main/java/roomescape/domain/reservationtime/admin/CreateTimeResponse.java +++ b/src/main/java/roomescape/domain/reservationtime/admin/dto/CreateTimeResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationtime.admin; +package roomescape.domain.reservationtime.admin.dto; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalTime; diff --git a/src/main/java/roomescape/domain/reservationtime/admin/ReservationTimeResponse.java b/src/main/java/roomescape/domain/reservationtime/admin/dto/ReservationTimeResponse.java similarity index 90% rename from src/main/java/roomescape/domain/reservationtime/admin/ReservationTimeResponse.java rename to src/main/java/roomescape/domain/reservationtime/admin/dto/ReservationTimeResponse.java index aa4fc9f994..7913a73f78 100644 --- a/src/main/java/roomescape/domain/reservationtime/admin/ReservationTimeResponse.java +++ b/src/main/java/roomescape/domain/reservationtime/admin/dto/ReservationTimeResponse.java @@ -1,4 +1,4 @@ -package roomescape.domain.reservationtime.admin; +package roomescape.domain.reservationtime.admin.dto; import com.fasterxml.jackson.annotation.JsonFormat; import java.time.LocalTime; diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java index d9e136d443..588fd1f5a0 100644 --- a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java @@ -12,10 +12,10 @@ import org.junit.jupiter.api.Test; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservationdate.ReservationDate; -import roomescape.domain.reservationtime.admin.CreateTimeRequest; -import roomescape.domain.reservationtime.admin.CreateTimeResponse; +import roomescape.domain.reservationtime.admin.dto.CreateTimeRequest; +import roomescape.domain.reservationtime.admin.dto.CreateTimeResponse; import roomescape.domain.reservationtime.dto.ReservationTimeAvailabilityResponse; -import roomescape.domain.reservationtime.admin.ReservationTimeResponse; +import roomescape.domain.reservationtime.admin.dto.ReservationTimeResponse; import roomescape.domain.theme.Theme; import roomescape.support.exception.NotFoundException; import roomescape.support.exception.RoomescapeException; diff --git a/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeControllerTest.java b/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeControllerTest.java index 5d0cf6ce64..67e17c4a52 100644 --- a/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeControllerTest.java +++ b/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeControllerTest.java @@ -24,6 +24,9 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import roomescape.domain.reservationtime.ReservationTimeService; +import roomescape.domain.reservationtime.admin.dto.CreateTimeRequest; +import roomescape.domain.reservationtime.admin.dto.CreateTimeResponse; +import roomescape.domain.reservationtime.admin.dto.ReservationTimeResponse; import roomescape.support.auth.AdminRequestValidator; import roomescape.support.exception.ConflictException; import roomescape.support.exception.errors.ReservationTimeErrors; From 82f6ff4af252e65dec5d9b554f8c0f7e43a7e23c Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 09:48:50 +0900 Subject: [PATCH 35/43] =?UTF-8?q?test:=20Theme=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/theme/ThemeControllerTest.java | 76 +++++++ .../theme/admin/AdminThemeControllerTest.java | 214 ++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 src/test/java/roomescape/domain/theme/ThemeControllerTest.java create mode 100644 src/test/java/roomescape/domain/theme/admin/AdminThemeControllerTest.java diff --git a/src/test/java/roomescape/domain/theme/ThemeControllerTest.java b/src/test/java/roomescape/domain/theme/ThemeControllerTest.java new file mode 100644 index 0000000000..3c4d5f77f7 --- /dev/null +++ b/src/test/java/roomescape/domain/theme/ThemeControllerTest.java @@ -0,0 +1,76 @@ +package roomescape.domain.theme; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import roomescape.domain.theme.dto.ThemeRankResponse; +import roomescape.domain.theme.dto.ThemeResponse; + +@WebMvcTest(ThemeController.class) +class ThemeControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ThemeService themeService; + + @Test + @DisplayName("전체 테마 조회의 요청과 응답을 확인한다.") + void getAllTheme() throws Exception { + // given + ThemeResponse response = new ThemeResponse( + 1L, + "공포", + "무서운 테마", + "theme-url" + ); + given(themeService.getAllTheme()) + .willReturn(List.of(response)); + + // when & then + mockMvc.perform(get("/themes") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("공포")) + .andExpect(jsonPath("$[0].content").value("무서운 테마")) + .andExpect(jsonPath("$[0].url").value("theme-url")); + + verify(themeService).getAllTheme(); + } + + @Test + @DisplayName("테마 랭킹 조회의 요청과 응답을 확인한다.") + void getThemeRank() throws Exception { + // given + ThemeRankResponse response = new ThemeRankResponse( + 1L, + "공포", + "theme-url" + ); + given(themeService.getThemeRank()) + .willReturn(List.of(response)); + + // when & then + mockMvc.perform(get("/themes/rank") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].themeName").value("공포")) + .andExpect(jsonPath("$[0].url").value("theme-url")); + + verify(themeService).getThemeRank(); + } +} diff --git a/src/test/java/roomescape/domain/theme/admin/AdminThemeControllerTest.java b/src/test/java/roomescape/domain/theme/admin/AdminThemeControllerTest.java new file mode 100644 index 0000000000..185617fe2f --- /dev/null +++ b/src/test/java/roomescape/domain/theme/admin/AdminThemeControllerTest.java @@ -0,0 +1,214 @@ +package roomescape.domain.theme.admin; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import roomescape.domain.theme.ThemeService; +import roomescape.domain.theme.admin.dto.AdminThemeResponse; +import roomescape.domain.theme.admin.dto.CreateThemeRequest; +import roomescape.domain.theme.admin.dto.CreateThemeResponse; +import roomescape.support.auth.AdminRequestValidator; +import roomescape.support.exception.ConflictException; +import roomescape.support.exception.errors.ThemeErrors; + +@WebMvcTest(AdminThemeController.class) +class AdminThemeControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ThemeService themeService; + + @MockitoBean + private AdminRequestValidator validator; + + @Test + @DisplayName("관리자가 전체 테마 조회 시 요청과 응답을 확인한다.") + void getAllThemeForAdmin() throws Exception { + // given + AdminThemeResponse response = new AdminThemeResponse( + 1L, + "공포", + "무서운 테마", + "theme-url" + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + given(themeService.getAllThemeForAdmin()) + .willReturn(List.of(response)); + + // when & then + mockMvc.perform(get("/admin/themes") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$[0].name").value("공포")) + .andExpect(jsonPath("$[0].content").value("무서운 테마")) + .andExpect(jsonPath("$[0].url").value("theme-url")); + + verify(themeService).getAllThemeForAdmin(); + } + + @Test + @DisplayName("관리자 인증에 실패하면 전체 테마 조회 시 401을 반환한다.") + void getAllThemeForAdminWhenUnauthorized() throws Exception { + // given + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(get("/admin/themes") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "wrong-token")) + .andExpect(status().isUnauthorized()); + + verify(themeService, never()).getAllThemeForAdmin(); + } + + @Test + @DisplayName("관리자가 테마 생성 시 요청과 응답을 확인한다.") + void createTheme() throws Exception { + // given + CreateThemeRequest request = new CreateThemeRequest( + "공포", + "무서운 테마", + "theme-url" + ); + CreateThemeResponse response = new CreateThemeResponse( + 1L, + "공포", + "무서운 테마", + "theme-url" + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + given(themeService.createTheme(any(CreateThemeRequest.class))) + .willReturn(response); + + // when & then + mockMvc.perform(post("/admin/themes") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("공포")) + .andExpect(jsonPath("$.content").value("무서운 테마")) + .andExpect(jsonPath("$.url").value("theme-url")); + + verify(themeService).createTheme(request); + } + + @Test + @DisplayName("관리자 인증에 실패하면 테마 생성 시 401을 반환한다.") + void createThemeWhenUnauthorized() throws Exception { + // given + CreateThemeRequest request = new CreateThemeRequest( + "공포", + "무서운 테마", + "theme-url" + ); + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(post("/admin/themes") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "wrong-token") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + + verify(themeService, never()).createTheme(any(CreateThemeRequest.class)); + } + + @Test + @DisplayName("테마 생성 시 필수 요청값이 누락되면 예외가 발생한다.") + void createThemeWithoutName() throws Exception { + // given + String request = """ + { + "name": "", + "content": "무서운 테마", + "url": "theme-url" + } + """; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + + // when & then + mockMvc.perform(post("/admin/themes") + .contentType(MediaType.APPLICATION_JSON) + .header("X-ADMIN-TOKEN", "token") + .content(request)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INPUT_VALIDATION_ERROR")) + .andExpect(jsonPath("$.message").value("테마 이름은 비어있을 수 없습니다.")); + } + + @Test + @DisplayName("관리자가 테마 삭제 시 요청과 응답을 확인한다.") + void deleteTheme() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + + // when & then + mockMvc.perform(delete("/admin/themes/{id}", id) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isNoContent()); + + verify(themeService).deleteTheme(id); + } + + @Test + @DisplayName("관리자 인증에 실패하면 테마 삭제 시 401을 반환한다.") + void deleteThemeWhenUnauthorized() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); + + // when & then + mockMvc.perform(delete("/admin/themes/{id}", id) + .header("X-ADMIN-TOKEN", "wrong-token")) + .andExpect(status().isUnauthorized()); + + verify(themeService, never()).deleteTheme(id); + } + + @Test + @DisplayName("이미 예약이 존재하는 테마는 삭제할 수 없다.") + void deleteThemeWhenThemeInUse() throws Exception { + // given + Long id = 1L; + when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); + willThrow(new ConflictException(ThemeErrors.THEME_IN_USE)) + .given(themeService) + .deleteTheme(id); + + // when & then + mockMvc.perform(delete("/admin/themes/{id}", id) + .header("X-ADMIN-TOKEN", "token")) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("THEME_IN_USE")) + .andExpect(jsonPath("$.message").value("이미 예약이 존재하는 테마는 삭제할 수 없습니다.")); + } +} From c799ec47cb1decef8a83aaa8a6e922efde610f67 Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 09:49:33 +0900 Subject: [PATCH 36/43] =?UTF-8?q?refactor:=20yml=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f43642f39e..46defae0db 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,3 +5,5 @@ spring: path: /h2-console datasource: url: jdbc:h2:mem:database + +token: "" From de703acfb3d0aa59d8e9995ab6b0f9fc5f9240c9 Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 13:59:16 +0900 Subject: [PATCH 37/43] =?UTF-8?q?test:=20Theme=20e2e=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=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 --- .../domain/theme/ThemeIntegrationTest.java | 52 ++++++ .../admin/AdminThemeIntegrationTest.java | 171 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java create mode 100644 src/test/java/roomescape/domain/theme/admin/AdminThemeIntegrationTest.java diff --git a/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java b/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java new file mode 100644 index 0000000000..c1490596d9 --- /dev/null +++ b/src/test/java/roomescape/domain/theme/ThemeIntegrationTest.java @@ -0,0 +1,52 @@ +package roomescape.domain.theme; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class ThemeIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + RestAssured.port = port; + jdbcTemplate.update("DELETE FROM reservation"); + jdbcTemplate.update("DELETE FROM reservation_date"); + jdbcTemplate.update("DELETE FROM reservation_time"); + jdbcTemplate.update("DELETE FROM theme"); + } + + @Test + @DisplayName("전체 테마 조회를 end-to-end로 확인한다.") + void getAllTheme() { + jdbcTemplate.update( + "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", + "공포", "무서운 테마", "theme-url" + ); + + given().log().all() + .contentType(ContentType.JSON) + .when().get("/themes") + .then().log().all() + .statusCode(200) + .body("[0].name", is("공포")) + .body("[0].content", is("무서운 테마")) + .body("[0].url", is("theme-url")); + } +} diff --git a/src/test/java/roomescape/domain/theme/admin/AdminThemeIntegrationTest.java b/src/test/java/roomescape/domain/theme/admin/AdminThemeIntegrationTest.java new file mode 100644 index 0000000000..8c4497c958 --- /dev/null +++ b/src/test/java/roomescape/domain/theme/admin/AdminThemeIntegrationTest.java @@ -0,0 +1,171 @@ +package roomescape.domain.theme.admin; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class AdminThemeIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Value("${token}") + private String adminToken; + + @BeforeEach + void setUp() { + RestAssured.port = port; + jdbcTemplate.update("DELETE FROM reservation"); + jdbcTemplate.update("DELETE FROM reservation_date"); + jdbcTemplate.update("DELETE FROM reservation_time"); + jdbcTemplate.update("DELETE FROM theme"); + } + + @Test + @DisplayName("관리자의 테마 전체 조회를 end-to-end로 확인한다.") + void getAllThemeForAdmin() { + jdbcTemplate.update( + "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", + "공포", "무서운 테마", "theme-url" + ); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/themes") + .then().log().all() + .statusCode(200) + .body("[0].name", is("공포")) + .body("[0].content", is("무서운 테마")) + .body("[0].url", is("theme-url")); + } + + @Test + @DisplayName("관리자가 토큰을 누락했을 경우 401 예외가 발생한다.") + void getAllThemeForAdminWithoutToken() { + given().log().all() + .contentType(ContentType.JSON) + .when().get("/admin/themes") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("관리자의 테마 생성을 end-to-end로 확인한다.") + void createTheme() { + String request = """ + { + "name" : "공포", + "content": "두렵다", + "url": "theme-scare" + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .body(request) + .when().post("/admin/themes") + .then().log().all() + .statusCode(201) + .body("name", is("공포")) + .body("content", is("두렵다")) + .body("url", is("theme-scare")); + + given() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/themes") + .then() + .statusCode(200) + .body("name", hasItem("공포")) + .body("content", hasItem("두렵다")) + .body("url", hasItem("theme-scare")); + } + + @Test + @DisplayName("관리자 테마 생성 시 내용 필드가 누락되었을 경우 400 에러가 발생한다.") + void createThemeWithOutContent() { + String request = """ + { + "name" : "공포", + "url": "theme-scare" + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .body(request) + .when().post("/admin/themes") + .then().log().all() + .statusCode(400) + .body("code", is("INPUT_VALIDATION_ERROR")) + .body("message", is("테마 내용은 비어있을 수 없습니다.")); + } + + @Test + @DisplayName("관리자 테마 생성 시 관리자 인증 토큰이 누락되었을 경우 401 에러가 발생한다.") + void createThemeWithOutToken() { + String request = """ + { + "name" : "공포", + "content": "두렵다", + "url": "theme-scare" + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/admin/themes") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("관리자의 테마 삭제를 end-to-end로 확인한다.") + void deleteTheme() { + jdbcTemplate.update( + "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", + "공포", "무서운 테마", "theme-url" + ); + + Long themeId = jdbcTemplate.queryForObject( + "SELECT id FROM theme WHERE name = ?", + Long.class, + "공포" + ); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().delete("/admin/themes/{id}", themeId) + .then().log().all() + .statusCode(204); + + given() + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/themes") + .then() + .statusCode(200) + .body("name", not(hasItem("공포"))); + } +} From 3b1c67222655c3cab8cb69aab1dbdd632f45b106 Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 13:59:26 +0900 Subject: [PATCH 38/43] =?UTF-8?q?test:=20ReservationTime=20e2e=20=ED=85=8C?= =?UTF-8?q?=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 --- .../ReservationTimeIntegrationTest.java | 133 +++++++++++++++ .../AdminReservationTimeIntegrationTest.java | 160 ++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 src/test/java/roomescape/domain/reservationtime/ReservationTimeIntegrationTest.java create mode 100644 src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeIntegrationTest.java diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeIntegrationTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeIntegrationTest.java new file mode 100644 index 0000000000..30b7d94b59 --- /dev/null +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeIntegrationTest.java @@ -0,0 +1,133 @@ +package roomescape.domain.reservationtime; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class ReservationTimeIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + RestAssured.port = port; + jdbcTemplate.update("DELETE FROM reservation"); + jdbcTemplate.update("DELETE FROM reservation_date"); + jdbcTemplate.update("DELETE FROM reservation_time"); + jdbcTemplate.update("DELETE FROM theme"); + } + + @Test + @DisplayName("예약 가능 시간 조회를 end-to-end로 확인한다.") + void getReservationTimeAvailability() { + jdbcTemplate.update( + "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", + "공포", "무서운 테마", "theme-url" + ); + jdbcTemplate.update( + "INSERT INTO reservation_date(date) VALUES (?)", + "2026-06-01" + ); + jdbcTemplate.update( + "INSERT INTO reservation_time(start_at) VALUES (?)", + "10:00" + ); + jdbcTemplate.update( + "INSERT INTO reservation_time(start_at) VALUES (?)", + "11:00" + ); + Long themeId = jdbcTemplate.queryForObject( + "SELECT id FROM theme WHERE name = ?", + Long.class, + "공포" + ); + Long dateId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_date WHERE date = ?", + Long.class, + "2026-06-01" + ); + Long firstTimeId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_time WHERE start_at = ?", + Long.class, + "10:00:00" + ); + Long secondTimeId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_time WHERE start_at = ?", + Long.class, + "11:00:00" + ); + jdbcTemplate.update( + "INSERT INTO reservation(name, date_id, time_id, theme_id) VALUES (?, ?, ?, ?)", + "보예", + dateId, + firstTimeId, + themeId + ); + + given().log().all() + .contentType(ContentType.JSON) + .param("themeId", themeId) + .param("dateId", dateId) + .when().get("/reservation-times/availability") + .then().log().all() + .statusCode(200) + .body("[0].timeId", is(firstTimeId.intValue())) + .body("[0].startAt", is("10:00")) + .body("[0].available", is(false)) + .body("[1].timeId", is(secondTimeId.intValue())) + .body("[1].startAt", is("11:00")) + .body("[1].available", is(true)); + } + + @Test + @DisplayName("예약 가능 시간 조회 시 themeId 파라미터가 누락되었을 경우 400 에러가 발생한다.") + void getReservationTimeAvailabilityWithoutThemeId() { + given().log().all() + .contentType(ContentType.JSON) + .param("dateId", 1L) + .when().get("/reservation-times/availability") + .then().log().all() + .statusCode(400) + .body("code", is("REQUIRED_PARAMETER_MISSING")) + .body("message", is("필수 요청 파라미터가 누락되었습니다.")); + } + + @Test + @DisplayName("예약 가능 시간 조회 시 존재하지 않는 테마일 경우 404 에러가 발생한다.") + void getReservationTimeAvailabilityWhenThemeNotFound() { + jdbcTemplate.update( + "INSERT INTO reservation_date(date) VALUES (?)", + "2026-06-01" + ); + Long dateId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_date WHERE date = ?", + Long.class, + "2026-06-01" + ); + + given().log().all() + .contentType(ContentType.JSON) + .param("themeId", 999L) + .param("dateId", dateId) + .when().get("/reservation-times/availability") + .then().log().all() + .statusCode(404) + .body("code", is("THEME_NOT_EXIST")) + .body("message", is("존재하지 않는 테마 입니다.")); + } +} diff --git a/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeIntegrationTest.java b/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeIntegrationTest.java new file mode 100644 index 0000000000..c67585d33f --- /dev/null +++ b/src/test/java/roomescape/domain/reservationtime/admin/AdminReservationTimeIntegrationTest.java @@ -0,0 +1,160 @@ +package roomescape.domain.reservationtime.admin; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class AdminReservationTimeIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Value("${token}") + private String adminToken; + + @BeforeEach + void setUp() { + RestAssured.port = port; + jdbcTemplate.update("DELETE FROM reservation"); + jdbcTemplate.update("DELETE FROM reservation_date"); + jdbcTemplate.update("DELETE FROM reservation_time"); + jdbcTemplate.update("DELETE FROM theme"); + } + + @Test + @DisplayName("관리자의 예약 시간 전체 조회를 end-to-end로 확인한다.") + void getAllReservationTime() { + jdbcTemplate.update( + "INSERT INTO reservation_time(start_at) VALUES (?)", + "10:10" + ); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/times") + .then().log().all() + .statusCode(200) + .body("[0].startAt", is("10:10")); + } + + @Test + @DisplayName("관리자가 토큰을 누락했을 경우 401 예외가 발생한다.") + void getAllReservationTimeWithoutToken() { + given().log().all() + .contentType(ContentType.JSON) + .when().get("/admin/times") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("관리자의 예약 시간 생성을 end-to-end로 확인한다.") + void createReservationTime() { + String request = """ + { + "startAt": "10:10" + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .body(request) + .when().post("/admin/times") + .then().log().all() + .statusCode(200) + .body("startAt", is("10:10")); + + given() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/times") + .then() + .statusCode(200) + .body("startAt", hasItem("10:10")); + } + + @Test + @DisplayName("관리자 예약 시간 생성 시 시작 시간이 누락되었을 경우 400 에러가 발생한다.") + void createReservationTimeWithoutStartAt() { + String request = """ + { + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .body(request) + .when().post("/admin/times") + .then().log().all() + .statusCode(400) + .body("code", is("INPUT_VALIDATION_ERROR")) + .body("message", is("시간은 필수 사항 입니다. 시간을 선택해주세요.")); + } + + @Test + @DisplayName("관리자 예약 시간 생성 시 관리자 인증 토큰이 누락되었을 경우 401 에러가 발생한다.") + void createReservationTimeWithoutToken() { + String request = """ + { + "startAt": "10:10" + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/admin/times") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("관리자의 예약 시간 삭제를 end-to-end로 확인한다.") + void deleteReservationTime() { + jdbcTemplate.update( + "INSERT INTO reservation_time(start_at) VALUES (?)", + "10:10" + ); + + Long timeId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_time WHERE start_at = ?", + Long.class, + "10:10:00" + ); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().delete("/admin/times/{id}", timeId) + .then().log().all() + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/times") + .then() + .statusCode(200) + .body("startAt", not(hasItem("10:10"))); + } +} From 5d2ff24ffb81dc8f161d2c20d0f52fa2fdeb54fc Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 14:01:59 +0900 Subject: [PATCH 39/43] =?UTF-8?q?test:=20ReservationDate=20e2e=20=ED=85=8C?= =?UTF-8?q?=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 --- .../ReservationDateIntegrationTest.java | 50 +++++ .../AdminReservationDateIntegrationTest.java | 212 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 src/test/java/roomescape/domain/reservationdate/ReservationDateIntegrationTest.java create mode 100644 src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateIntegrationTest.java diff --git a/src/test/java/roomescape/domain/reservationdate/ReservationDateIntegrationTest.java b/src/test/java/roomescape/domain/reservationdate/ReservationDateIntegrationTest.java new file mode 100644 index 0000000000..aeaee558c9 --- /dev/null +++ b/src/test/java/roomescape/domain/reservationdate/ReservationDateIntegrationTest.java @@ -0,0 +1,50 @@ +package roomescape.domain.reservationdate; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class ReservationDateIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + RestAssured.port = port; + jdbcTemplate.update("DELETE FROM reservation"); + jdbcTemplate.update("DELETE FROM reservation_date"); + jdbcTemplate.update("DELETE FROM reservation_time"); + jdbcTemplate.update("DELETE FROM theme"); + } + + @Test + @DisplayName("전체 예약 날짜 조회를 end-to-end로 확인한다.") + void getAllReservationDates() { + jdbcTemplate.update( + "INSERT INTO reservation_date(date) VALUES (?)", + "2026-06-01" + ); + + given().log().all() + .contentType(ContentType.JSON) + .when().get("/reservation-dates") + .then().log().all() + .statusCode(200) + .body("[0].reservationDate", is("2026-06-01")); + } +} diff --git a/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateIntegrationTest.java b/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateIntegrationTest.java new file mode 100644 index 0000000000..8fc15aad6c --- /dev/null +++ b/src/test/java/roomescape/domain/reservationdate/admin/AdminReservationDateIntegrationTest.java @@ -0,0 +1,212 @@ +package roomescape.domain.reservationdate.admin; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class AdminReservationDateIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Value("${token}") + private String adminToken; + + @BeforeEach + void setUp() { + RestAssured.port = port; + jdbcTemplate.update("DELETE FROM reservation"); + jdbcTemplate.update("DELETE FROM reservation_date"); + jdbcTemplate.update("DELETE FROM reservation_time"); + jdbcTemplate.update("DELETE FROM theme"); + } + + @Test + @DisplayName("관리자의 예약 날짜 전체 조회를 end-to-end로 확인한다.") + void getAllReservationDateForAdmin() { + jdbcTemplate.update( + "INSERT INTO reservation_date(date) VALUES (?)", + "2026-06-01" + ); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/reservation-dates") + .then().log().all() + .statusCode(200) + .body("[0].reservationDate", is("2026-06-01")); + } + + @Test + @DisplayName("관리자가 토큰을 누락했을 경우 401 예외가 발생한다.") + void getAllReservationDateForAdminWithoutToken() { + given().log().all() + .contentType(ContentType.JSON) + .when().get("/admin/reservation-dates") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("관리자의 예약 날짜 생성을 end-to-end로 확인한다.") + void createReservationDate() { + String request = """ + { + "reservationDate": "2026-06-01" + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .body(request) + .when().post("/admin/reservation-dates") + .then().log().all() + .statusCode(201) + .body("reservationDate", is("2026-06-01")); + + given() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/reservation-dates") + .then() + .statusCode(200) + .body("reservationDate", hasItem("2026-06-01")); + } + + @Test + @DisplayName("관리자 예약 날짜 생성 시 날짜 필드가 누락되었을 경우 400 에러가 발생한다.") + void createReservationDateWithoutDate() { + String request = """ + { + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .body(request) + .when().post("/admin/reservation-dates") + .then().log().all() + .statusCode(400) + .body("code", is("INPUT_VALIDATION_ERROR")) + .body("message", is("예약 날짜는 필수 사항 입니다. 날짜를 선택해주세요.")); + } + + @Test + @DisplayName("관리자 예약 날짜 생성 시 관리자 인증 토큰이 누락되었을 경우 401 에러가 발생한다.") + void createReservationDateWithoutToken() { + String request = """ + { + "reservationDate": "2026-06-01" + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/admin/reservation-dates") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("관리자의 예약 날짜 삭제를 end-to-end로 확인한다.") + void deleteReservationDate() { + jdbcTemplate.update( + "INSERT INTO reservation_date(date) VALUES (?)", + "2026-06-01" + ); + + Long dateId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_date WHERE date = ?", + Long.class, + "2026-06-01" + ); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().delete("/admin/reservation-dates/{id}", dateId) + .then().log().all() + .statusCode(204); + + given() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/reservation-dates") + .then() + .statusCode(200) + .body("reservationDate", not(hasItem("2026-06-01"))); + } + + @Test + @DisplayName("이미 예약이 존재하는 날짜는 삭제할 수 없다.") + void deleteReservationDateWhenDateInUse() { + jdbcTemplate.update( + "INSERT INTO reservation_date(date) VALUES (?)", + "2026-06-01" + ); + jdbcTemplate.update( + "INSERT INTO reservation_time(start_at) VALUES (?)", + "10:00" + ); + jdbcTemplate.update( + "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", + "공포", + "무서운 테마", + "theme-url" + ); + + Long dateId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_date WHERE date = ?", + Long.class, + "2026-06-01" + ); + Long timeId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_time WHERE start_at = ?", + Long.class, + "10:00:00" + ); + Long themeId = jdbcTemplate.queryForObject( + "SELECT id FROM theme WHERE name = ?", + Long.class, + "공포" + ); + + jdbcTemplate.update( + "INSERT INTO reservation(name, date_id, time_id, theme_id) VALUES (?, ?, ?, ?)", + "보예", + dateId, + timeId, + themeId + ); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().delete("/admin/reservation-dates/{id}", dateId) + .then().log().all() + .statusCode(409) + .body("code", is("RESERVATION_DATE_IN_USE")) + .body("message", is("이미 예약이 존재하는 날짜는 삭제할 수 없습니다.")); + } +} From f0af145aeed30f4a261a2031aea18591d59fd3d6 Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 14:02:06 +0900 Subject: [PATCH 40/43] =?UTF-8?q?test:=20Reservation=20e2e=20=ED=85=8C?= =?UTF-8?q?=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 --- .../ReservationIntegrationTest.java | 237 ++++++++++++++++++ .../AdminReservationIntegrationTest.java | 133 ++++++++++ 2 files changed, 370 insertions(+) create mode 100644 src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java create mode 100644 src/test/java/roomescape/domain/reservation/admin/AdminReservationIntegrationTest.java diff --git a/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java b/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java new file mode 100644 index 0000000000..cef2b4f3d3 --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/ReservationIntegrationTest.java @@ -0,0 +1,237 @@ +package roomescape.domain.reservation; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class ReservationIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + RestAssured.port = port; + jdbcTemplate.update("DELETE FROM reservation"); + jdbcTemplate.update("DELETE FROM reservation_date"); + jdbcTemplate.update("DELETE FROM reservation_time"); + jdbcTemplate.update("DELETE FROM theme"); + } + + @Test + @DisplayName("예약 생성을 end-to-end로 확인한다.") + void createReservation() { + Long themeId = saveTheme("공포"); + Long dateId = saveDate("2026-06-01"); + Long timeId = saveTime("10:00"); + + String request = """ + { + "name": "보예", + "dateId": %d, + "timeId": %d, + "themeId": %d + } + """.formatted(dateId, timeId, themeId); + + given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/reservations") + .then().log().all() + .statusCode(201) + .body("name", is("보예")) + .body("date", is("2026-06-01")) + .body("time", is("10:00")) + .body("theme.name", is("공포")) + .body("theme.content", is("무서운 테마")) + .body("theme.url", is("theme-url")); + + given() + .contentType(ContentType.JSON) + .param("name", "보예") + .when().get("/reservations") + .then() + .statusCode(200) + .body("name", is("보예")) + .body("reservation", hasSize(1)); + } + + @Test + @DisplayName("예약 생성 시 시간 필드가 누락되었을 경우 400 에러가 발생한다.") + void createReservationWithoutTimeId() { + Long themeId = saveTheme("공포"); + Long dateId = saveDate("2026-06-01"); + + String request = """ + { + "name": "보예", + "dateId": %d, + "themeId": %d + } + """.formatted(dateId, themeId); + + given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/reservations") + .then().log().all() + .statusCode(400) + .body("code", is("INPUT_VALIDATION_ERROR")) + .body("message", is("시간은 필수 선택 사항 입니다. 시간을 선택해주세요.")); + } + + @Test + @DisplayName("예약자 이름으로 예약 조회를 end-to-end로 확인한다.") + void getUserReservations() { + saveReservation("보예", "2026-06-01", "10:00", "공포"); + + given().log().all() + .contentType(ContentType.JSON) + .param("name", "보예") + .when().get("/reservations") + .then().log().all() + .statusCode(200) + .body("name", is("보예")) + .body("reservation[0].date.startWhen", is("2026-06-01")) + .body("reservation[0].time.startAt", is("10:00")) + .body("reservation[0].theme.name", is("공포")); + } + + @Test + @DisplayName("예약 조회 시 이름 파라미터가 누락되었을 경우 400 에러가 발생한다.") + void getUserReservationsWithoutName() { + given().log().all() + .contentType(ContentType.JSON) + .when().get("/reservations") + .then().log().all() + .statusCode(400) + .body("code", is("REQUIRED_PARAMETER_MISSING")) + .body("message", is("필수 요청 파라미터가 누락되었습니다.")); + } + + @Test + @DisplayName("예약 삭제를 end-to-end로 확인한다.") + void deleteUserReservation() { + Long reservationId = saveReservation("보예", "2026-06-01", "10:00", "공포"); + + given().log().all() + .contentType(ContentType.JSON) + .when().delete("/reservations/{id}", reservationId) + .then().log().all() + .statusCode(204); + + given() + .contentType(ContentType.JSON) + .param("name", "보예") + .when().get("/reservations") + .then() + .statusCode(200) + .body("name", is("보예")) + .body("reservation", hasSize(0)); + } + + @Test + @DisplayName("예약 수정을 end-to-end로 확인한다.") + void updateReservation() { + Long reservationId = saveReservation("보예", "2026-06-01", "10:00", "공포"); + saveDate("2026-06-02"); + saveTime("11:00"); + + String request = """ + { + "startWhen": "2026-06-02", + "startAt": "11:00" + } + """; + + given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().patch("/reservations/{id}", reservationId) + .then().log().all() + .statusCode(204); + + given() + .contentType(ContentType.JSON) + .param("name", "보예") + .when().get("/reservations") + .then() + .statusCode(200) + .body("reservation[0].date.startWhen", is("2026-06-02")) + .body("reservation[0].time.startAt", is("11:00")); + } + + private Long saveReservation(String name, String date, String time, String themeName) { + Long themeId = saveTheme(themeName); + Long dateId = saveDate(date); + Long timeId = saveTime(time); + + jdbcTemplate.update( + "INSERT INTO reservation(name, date_id, time_id, theme_id) VALUES (?, ?, ?, ?)", + name, + dateId, + timeId, + themeId + ); + + return jdbcTemplate.queryForObject( + "SELECT id FROM reservation WHERE name = ?", + Long.class, + name + ); + } + + private Long saveTheme(String themeName) { + jdbcTemplate.update( + "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", + themeName, + "무서운 테마", + "theme-url" + ); + return jdbcTemplate.queryForObject( + "SELECT id FROM theme WHERE name = ?", + Long.class, + themeName + ); + } + + private Long saveDate(String date) { + jdbcTemplate.update( + "INSERT INTO reservation_date(date) VALUES (?)", + date + ); + return jdbcTemplate.queryForObject( + "SELECT id FROM reservation_date WHERE date = ?", + Long.class, + date + ); + } + + private Long saveTime(String time) { + jdbcTemplate.update( + "INSERT INTO reservation_time(start_at) VALUES (?)", + time + ); + return jdbcTemplate.queryForObject( + "SELECT id FROM reservation_time WHERE start_at = ?", + Long.class, + time + ":00" + ); + } +} diff --git a/src/test/java/roomescape/domain/reservation/admin/AdminReservationIntegrationTest.java b/src/test/java/roomescape/domain/reservation/admin/AdminReservationIntegrationTest.java new file mode 100644 index 0000000000..a1b33f4390 --- /dev/null +++ b/src/test/java/roomescape/domain/reservation/admin/AdminReservationIntegrationTest.java @@ -0,0 +1,133 @@ +package roomescape.domain.reservation.admin; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.jdbc.core.JdbcTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class AdminReservationIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Value("${token}") + private String adminToken; + + @BeforeEach + void setUp() { + RestAssured.port = port; + jdbcTemplate.update("DELETE FROM reservation"); + jdbcTemplate.update("DELETE FROM reservation_date"); + jdbcTemplate.update("DELETE FROM reservation_time"); + jdbcTemplate.update("DELETE FROM theme"); + } + + @Test + @DisplayName("관리자의 예약 전체 조회를 end-to-end로 확인한다.") + void getAllReservation() { + saveThemeDateTimeAndReservation("보예"); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/reservations") + .then().log().all() + .statusCode(200) + .body("[0].name", is("보예")) + .body("[0].date", is("2026-06-01")) + .body("[0].time.startAt", is("10:00")) + .body("[0].theme.name", is("공포")); + } + + @Test + @DisplayName("관리자가 토큰을 누락했을 경우 401 예외가 발생한다.") + void getAllReservationWithoutToken() { + given().log().all() + .contentType(ContentType.JSON) + .when().get("/admin/reservations") + .then().log().all() + .statusCode(401); + } + + @Test + @DisplayName("관리자의 예약 삭제를 end-to-end로 확인한다.") + void deleteReservation() { + Long reservationId = saveThemeDateTimeAndReservation("보예"); + + given().log().all() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().delete("/admin/reservations/{id}", reservationId) + .then().log().all() + .statusCode(204); + + given() + .contentType(ContentType.JSON) + .header("X-ADMIN-TOKEN", adminToken) + .when().get("/admin/reservations") + .then() + .statusCode(200) + .body("size()", is(0)); + } + + private Long saveThemeDateTimeAndReservation(String name) { + jdbcTemplate.update( + "INSERT INTO theme(name, content, url) VALUES (?, ?, ?)", + "공포", + "무서운 테마", + "theme-url" + ); + jdbcTemplate.update( + "INSERT INTO reservation_date(date) VALUES (?)", + "2026-06-01" + ); + jdbcTemplate.update( + "INSERT INTO reservation_time(start_at) VALUES (?)", + "10:00" + ); + + Long themeId = jdbcTemplate.queryForObject( + "SELECT id FROM theme WHERE name = ?", + Long.class, + "공포" + ); + Long dateId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_date WHERE date = ?", + Long.class, + "2026-06-01" + ); + Long timeId = jdbcTemplate.queryForObject( + "SELECT id FROM reservation_time WHERE start_at = ?", + Long.class, + "10:00:00" + ); + + jdbcTemplate.update( + "INSERT INTO reservation(name, date_id, time_id, theme_id) VALUES (?, ?, ?, ?)", + name, + dateId, + timeId, + themeId + ); + + return jdbcTemplate.queryForObject( + "SELECT id FROM reservation WHERE name = ?", + Long.class, + name + ); + } +} From 877355c2c4a5f29bc8f6545e27dba653def5d03b Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 14:23:39 +0900 Subject: [PATCH 41/43] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=BB=A8=EB=B2=A4=EC=85=98=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationServiceTest.java | 67 +++++++++++++------ .../domain/reservation/ReservationTest.java | 28 +++++--- .../ReservationDateServiceTest.java | 13 ++-- .../ReservationTimeServiceTest.java | 22 ++++-- .../reservationtime/ReservationTimeTest.java | 7 +- .../domain/theme/ThemeServiceTest.java | 13 ++-- .../roomescape/domain/theme/ThemeTest.java | 7 +- .../exception/GlobalExceptionHandlerTest.java | 7 +- 8 files changed, 112 insertions(+), 52 deletions(-) diff --git a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java index ca9a441ad4..b0827f29eb 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationServiceTest.java @@ -12,6 +12,7 @@ import java.time.ZoneId; import java.util.List; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.domain.reservation.admin.dto.ReservationResponse; import roomescape.domain.reservation.dto.CreateReservationRequest; @@ -45,7 +46,8 @@ void setUp() { } @Test - void 존재하는_예약_시간으로_예약을_생성한다() { + @DisplayName("존재하는 예약 시간으로 예약을 생성한다.") + void createReservationWithExistingReservationTime() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = ReservationTime.createWithoutId(LocalTime.of(10, 0)); @@ -80,7 +82,8 @@ void setUp() { } @Test - void 존재하지_않는_예약_시간으로_예약을_생성하면_예외가_발생한다() { + @DisplayName("존재하지 않는 예약 시간으로 예약을 생성하면 예외가 발생한다.") + void throwExceptionWhenCreatingReservationWithNonExistentReservationTime() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationService reservationService = new ReservationService( @@ -99,7 +102,8 @@ void setUp() { } @Test - void 존재하지_않는_테마로_예약을_생성하면_예외가_발생한다() { + @DisplayName("존재하지 않는 테마로 예약을 생성하면 예외가 발생한다.") + void throwExceptionWhenCreatingReservationWithNonExistentTheme() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -127,7 +131,8 @@ void setUp() { } @Test - void 예약_목록을_전체_조회한다() { + @DisplayName("예약 목록을 전체 조회한다.") + void getAllReservations() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationDate savedReservationDate = reservationDateRepository.save( @@ -169,7 +174,8 @@ void setUp() { } @Test - void 사용자가_이름으로_예약을_조회한다() { + @DisplayName("사용자가 이름으로 예약을 조회한다.") + void getUserReservationsByName() { // given String name = "보예짱"; Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); @@ -228,7 +234,8 @@ void setUp() { } @Test - void 오늘보다_이전_날짜는_예약할_수_없다() { + @DisplayName("오늘보다 이전 날짜는 예약할 수 없다.") + void throwExceptionWhenCreatingReservationBeforeToday() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -259,7 +266,8 @@ void setUp() { } @Test - void 오늘_예약일_경우_현재_시간_이전은_예약할_수_없다() { + @DisplayName("오늘 예약일 경우 현재 시간 이전은 예약할 수 없다.") + void throwExceptionWhenCreatingReservationBeforeCurrentTimeOnToday() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime beforeNow = reservationTimeRepository.save( @@ -290,7 +298,8 @@ void setUp() { } @Test - void 오늘_예약이지만_현재_시간은_예약할_수_있다() { + @DisplayName("오늘 예약이지만 현재 시간은 예약할 수 있다.") + void createReservationAtCurrentTimeOnToday() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime nowTime = reservationTimeRepository.save( @@ -332,7 +341,8 @@ void setUp() { } @Test - void 날짜가_오늘_이후이고_현재_시간보다_이전이면_정상_예약_된다() { + @DisplayName("날짜가 오늘 이후이고 현재 시간보다 이전이면 정상 예약 된다.") + void createReservationAfterTodayEvenIfTimeIsBeforeNow() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -374,7 +384,8 @@ void setUp() { } @Test - void 중복된_예약은_예외가_발생한다() { + @DisplayName("중복된 예약은 예외가 발생한다.") + void throwExceptionWhenCreatingDuplicatedReservation() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = ReservationTime.createWithoutId(LocalTime.of(10, 0)); @@ -404,7 +415,8 @@ void setUp() { } @Test - void 사용자는_미래_예약을_삭제할_수_있다() { + @DisplayName("사용자는 미래 예약을 삭제할 수 있다.") + void deleteFutureReservationForUser() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -433,7 +445,8 @@ void setUp() { } @Test - void 사용자는_이미_시간이_지난_예약을_삭제할_수_없다() { + @DisplayName("사용자는 이미 시간이 지난 예약을 삭제할 수 없다.") + void throwExceptionWhenUserDeletesPastTimeReservation() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -461,7 +474,8 @@ void setUp() { } @Test - void 사용자는_이미_날짜가_지난_예약을_삭제할_수_없다() { + @DisplayName("사용자는 이미 날짜가 지난 예약을 삭제할 수 없다.") + void throwExceptionWhenUserDeletesPastDateReservation() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -489,7 +503,8 @@ void setUp() { } @Test - void 사용자가_존재하지_않는_예약을_삭제하면_예외가_발생한다() { + @DisplayName("사용자가 존재하지 않는 예약을 삭제하면 예외가 발생한다.") + void throwExceptionWhenUserDeletesNonExistentReservation() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationService reservationService = new ReservationService( @@ -507,7 +522,8 @@ void setUp() { } @Test - void 예약_날짜와_시간을_수정한다() { + @DisplayName("예약 날짜와 시간을 수정한다.") + void updateReservationDateAndTime() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime beforeReservationTime = reservationTimeRepository.save( @@ -552,7 +568,8 @@ void setUp() { } @Test - void 예약_시간만_수정한다() { + @DisplayName("예약 시간만 수정한다.") + void updateReservationTimeOnly() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime beforeReservationTime = reservationTimeRepository.save( @@ -589,7 +606,8 @@ void setUp() { } @Test - void 존재하지_않는_예약을_수정하면_예외가_발생한다() { + @DisplayName("존재하지 않는 예약을 수정하면 예외가 발생한다.") + void throwExceptionWhenUpdatingNonExistentReservation() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationService reservationService = new ReservationService( @@ -608,7 +626,8 @@ void setUp() { } @Test - void 존재하지_않는_예약_날짜로_수정하면_예외가_발생한다() { + @DisplayName("존재하지 않는 예약 날짜로 수정하면 예외가 발생한다.") + void throwExceptionWhenUpdatingReservationWithNonExistentDate() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -637,7 +656,8 @@ void setUp() { } @Test - void 존재하지_않는_예약_시간으로_수정하면_예외가_발생한다() { + @DisplayName("존재하지 않는 예약 시간으로 수정하면 예외가 발생한다.") + void throwExceptionWhenUpdatingReservationWithNonExistentTime() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -666,7 +686,8 @@ void setUp() { } @Test - void 오늘보다_이전_날짜로_예약을_수정할_수_없다() { + @DisplayName("오늘보다 이전 날짜로 예약을 수정할 수 없다.") + void throwExceptionWhenUpdatingReservationToDateBeforeToday() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -698,7 +719,8 @@ void setUp() { } @Test - void 오늘_예약을_현재_시간보다_이전으로_수정할_수_없다() { + @DisplayName("오늘 예약을 현재 시간보다 이전으로 수정할 수 없다.") + void throwExceptionWhenUpdatingReservationToTimeBeforeNowOnToday() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( @@ -730,7 +752,8 @@ void setUp() { } @Test - void 중복된_예약으로_수정하면_예외가_발생한다() { + @DisplayName("중복된 예약으로 수정하면 예외가 발생한다.") + void throwExceptionWhenUpdatingReservationToDuplicatedSchedule() { // given Clock now = fixedClockAt(LocalDateTime.of(2026, 5, 12, 13, 0)); ReservationTime reservationTime = reservationTimeRepository.save( diff --git a/src/test/java/roomescape/domain/reservation/ReservationTest.java b/src/test/java/roomescape/domain/reservation/ReservationTest.java index 475d486203..c4f8e442d8 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationTest.java @@ -6,6 +6,7 @@ import java.time.LocalDate; import java.time.LocalTime; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.domain.reservationdate.ReservationDate; import roomescape.domain.reservationtime.ReservationTime; @@ -15,7 +16,8 @@ class ReservationTest { @Test - void id가_없는_예약을_생성한다() { + @DisplayName("id가 없는 예약을 생성한다.") + void createReservationWithoutId() { // given String name = "보예"; ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); @@ -37,7 +39,8 @@ class ReservationTest { } @Test - void id를_부여한_예약을_생성한다() { + @DisplayName("id를 부여한 예약을 생성한다.") + void createReservationWithId() { // given ReservationTime time = ReservationTime.createWithoutId(LocalTime.of(15, 40)); ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); @@ -70,7 +73,8 @@ class ReservationTest { } @Test - void DB에서_조회한_예약을_생성한다() { + @DisplayName("DB에서 조회한 예약을 생성한다.") + void createReservationLoadedFromDatabase() { // given long id = 1L; String name = "보예"; @@ -93,7 +97,8 @@ class ReservationTest { } @Test - void 이름이_null이면_예외가_발생한다() { + @DisplayName("이름이 null이면 예외가 발생한다.") + void throwExceptionWhenNameIsNull() { // given String name = null; ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); @@ -107,7 +112,8 @@ class ReservationTest { } @Test - void 이름이_공백이면_예외가_발생한다() { + @DisplayName("이름이 공백이면 예외가 발생한다.") + void throwExceptionWhenNameIsBlank() { // given String name = " "; ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); @@ -121,7 +127,8 @@ class ReservationTest { } @Test - void 이름이_10자를_초과하면_예외가_발생한다() { + @DisplayName("이름이 10자를 초과하면 예외가 발생한다.") + void throwExceptionWhenNameExceedsTenCharacters() { // given String name = "보예보예보예보예보예보"; ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); @@ -135,7 +142,8 @@ class ReservationTest { } @Test - void 날짜가_null이면_예외가_발생한다() { + @DisplayName("날짜가 null이면 예외가 발생한다.") + void throwExceptionWhenDateIsNull() { // given String name = "보예"; ReservationDate date = null; @@ -149,7 +157,8 @@ class ReservationTest { } @Test - void 예약_시간이_null이면_예외가_발생한다() { + @DisplayName("예약 시간이 null이면 예외가 발생한다.") + void throwExceptionWhenReservationTimeIsNull() { // given String name = "보예"; ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); @@ -163,7 +172,8 @@ class ReservationTest { } @Test - void 테마가_null이면_예외가_발생한다() { + @DisplayName("테마가 null이면 예외가 발생한다.") + void throwExceptionWhenThemeIsNull() { // given String name = "보예"; ReservationDate date = ReservationDate.createWithoutId(LocalDate.of(2023, 8, 5)); diff --git a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java index fcc612e197..7923ebd44f 100644 --- a/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java +++ b/src/test/java/roomescape/domain/reservationdate/ReservationDateServiceTest.java @@ -8,6 +8,7 @@ import java.time.LocalTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservationdate.admin.dto.AdminReservationDateResponse; @@ -31,7 +32,8 @@ void setUp() { } @Test - void 예약_날짜를_생성한다() { + @DisplayName("예약 날짜를 생성한다.") + void createReservationDate() { // given ReservationDateService reservationDateService = new ReservationDateService( reservationRepository, @@ -53,7 +55,8 @@ void setUp() { } @Test - void 예약_날짜_목록을_조회한다() { + @DisplayName("예약 날짜 목록을 조회한다.") + void getReservationDateList() { // given ReservationDate reservationDate = reservationDateRepository.save( ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); @@ -74,7 +77,8 @@ void setUp() { } @Test - void 이미_예약이_존재하는_날짜는_삭제할_수_없다() { + @DisplayName("이미 예약이 존재하는 날짜는 삭제할 수 없다.") + void throwExceptionWhenDeletingDateInUse() { // given ReservationDate reservationDate = reservationDateRepository.save( ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); @@ -98,7 +102,8 @@ void setUp() { } @Test - void 예약이_없는_날짜는_삭제한다() { + @DisplayName("예약이 없는 날짜는 삭제한다.") + void deleteDateWhenNoReservationExists() { // given ReservationDate reservationDate = reservationDateRepository.save( ReservationDate.createWithoutId(LocalDate.of(2026, 5, 4))); diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java index 588fd1f5a0..4c83f51fa6 100644 --- a/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeServiceTest.java @@ -9,6 +9,7 @@ import java.time.LocalTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.domain.reservation.Reservation; import roomescape.domain.reservationdate.ReservationDate; @@ -40,7 +41,8 @@ void setUp() { } @Test - void 예약_시간을_생성한다() { + @DisplayName("예약 시간을 생성한다.") + void createReservationTime() { // given ReservationTimeService reservationTimeService = createReservationTimeService(); @@ -59,7 +61,8 @@ void setUp() { } @Test - void 예약_시간_목록을_조회한다() { + @DisplayName("예약 시간 목록을 조회한다.") + void getReservationTimeList() { // given reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(10, 0))); reservationTimeRepository.save(ReservationTime.createWithoutId(LocalTime.of(11, 0))); @@ -81,7 +84,8 @@ void setUp() { } @Test - void 이미_예약이_존재하는_시간은_삭제할_수_없다() { + @DisplayName("이미 예약이 존재하는 시간은 삭제할 수 없다.") + void throwExceptionWhenDeletingTimeInUse() { // given ReservationTime reservationTime = reservationTimeRepository.save( ReservationTime.createWithoutId(LocalTime.of(10, 0)) @@ -103,7 +107,8 @@ void setUp() { } @Test - void 예약이_없는_시간은_삭제한다() { + @DisplayName("예약이 없는 시간은 삭제한다.") + void deleteTimeWhenNoReservationExists() { // given ReservationTime reservationTime = reservationTimeRepository.save( ReservationTime.createWithoutId(LocalTime.of(10, 0)) @@ -118,7 +123,8 @@ void setUp() { } @Test - void 예약_가능_시간을_조회한다() { + @DisplayName("예약 가능 시간을 조회한다.") + void getReservationTimeAvailability() { // given ReservationTime firstReservationTime = reservationTimeRepository.save( ReservationTime.createWithoutId(LocalTime.of(10, 0)) @@ -155,7 +161,8 @@ void setUp() { } @Test - void 존재하지_않는_테마로_예약_가능_시간을_조회할_수_없다() { + @DisplayName("존재하지 않는 테마로 예약 가능 시간을 조회할 수 없다.") + void throwExceptionWhenThemeDoesNotExistForAvailability() { // given ReservationDate reservationDate = reservationDateRepository.save( ReservationDate.createWithoutId(LocalDate.of(2026, 5, 16)) @@ -169,7 +176,8 @@ void setUp() { } @Test - void 존재하지_않는_날짜로_예약_가능_시간을_조회할_수_없다() { + @DisplayName("존재하지 않는 날짜로 예약 가능 시간을 조회할 수 없다.") + void throwExceptionWhenDateDoesNotExistForAvailability() { // given Theme theme = themeRepository.save(Theme.createWithoutId("공포", "무서운 테마", "theme-url")); ReservationTimeService reservationTimeService = createReservationTimeService(); diff --git a/src/test/java/roomescape/domain/reservationtime/ReservationTimeTest.java b/src/test/java/roomescape/domain/reservationtime/ReservationTimeTest.java index 5254804eaa..5f54d82a20 100644 --- a/src/test/java/roomescape/domain/reservationtime/ReservationTimeTest.java +++ b/src/test/java/roomescape/domain/reservationtime/ReservationTimeTest.java @@ -4,13 +4,15 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.time.LocalTime; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.support.exception.RoomescapeException; class ReservationTimeTest { @Test - void id가_없는_예약_시간을_생성한다() { + @DisplayName("id가 없는 예약 시간을 생성한다.") + void createReservationTimeWithoutId() { // given LocalTime startAt = LocalTime.of(10, 0); @@ -25,7 +27,8 @@ class ReservationTimeTest { } @Test - void 예약_시간이_null이면_예외가_발생한다() { + @DisplayName("예약 시간이 null이면 예외가 발생한다.") + void throwExceptionWhenReservationTimeIsNull() { // given LocalTime startAt = null; diff --git a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java index d38807f1d3..fd7cc9b28a 100644 --- a/src/test/java/roomescape/domain/theme/ThemeServiceTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeServiceTest.java @@ -5,6 +5,7 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.domain.theme.admin.dto.AdminThemeResponse; import roomescape.domain.theme.admin.dto.CreateThemeRequest; @@ -25,7 +26,8 @@ void setUp() { } @Test - void 관리자용_테마_목록을_조회한다() { + @DisplayName("관리자용 테마 목록을 조회한다.") + void getThemeListForAdmin() { // given themeRepository.save(Theme.createWithoutId("미스터리", "보예의 미스터리", "theme-url")); ThemeService themeService = new ThemeService(themeRepository, reservationRepository); @@ -44,7 +46,8 @@ void setUp() { } @Test - void 사용자용_테마_목록을_조회한다() { + @DisplayName("사용자용 테마 목록을 조회한다.") + void getThemeListForUser() { // given themeRepository.save(Theme.createWithoutId("미스터리", "보예의 미스터리", "theme-url")); ThemeService themeService = new ThemeService(themeRepository, reservationRepository); @@ -63,7 +66,8 @@ void setUp() { } @Test - void 테마를_생성한다() { + @DisplayName("테마를 생성한다.") + void createTheme() { // given ThemeService themeService = new ThemeService(themeRepository, reservationRepository); @@ -86,7 +90,8 @@ void setUp() { } @Test - void 테마를_삭제한다() { + @DisplayName("테마를 삭제한다.") + void deleteTheme() { // given Theme theme = themeRepository.save( Theme.createWithoutId("공포", "무섭다", "theme-url") diff --git a/src/test/java/roomescape/domain/theme/ThemeTest.java b/src/test/java/roomescape/domain/theme/ThemeTest.java index 9b6b71f52b..d7ad7fe6fa 100644 --- a/src/test/java/roomescape/domain/theme/ThemeTest.java +++ b/src/test/java/roomescape/domain/theme/ThemeTest.java @@ -4,13 +4,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import roomescape.support.exception.RoomescapeException; class ThemeTest { @Test - void id가_없는_테마를_생성한다() { + @DisplayName("id가 없는 테마를 생성한다.") + void createThemeWithoutId() { // given String name = "미스터리"; String content = "보예의 미스터리"; @@ -29,7 +31,8 @@ class ThemeTest { } @Test - void 이름이_10자를_초과하면_예외가_발생한다() { + @DisplayName("이름이 10자를 초과하면 예외가 발생한다.") + void throwExceptionWhenNameExceedsTenCharacters() { // given String name = "공포공포공포공포공포공"; diff --git a/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java b/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java index c81768664f..f25362180d 100644 --- a/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java +++ b/src/test/java/roomescape/support/exception/GlobalExceptionHandlerTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; import roomescape.support.exception.errors.ReservationErrors; @@ -9,7 +10,8 @@ class GlobalExceptionHandlerTest { @Test - void RoomescapeException을_에러_응답으로_변환한다() { + @DisplayName("RoomescapeException을 에러 응답으로 변환한다.") + void convertRoomescapeExceptionToErrorResponse() { // given GlobalExceptionHandler globalExceptionHandler = new GlobalExceptionHandler(); BadRequestException exception = new BadRequestException(ReservationErrors.INVALID_RESERVATION_NAME); @@ -26,7 +28,8 @@ class GlobalExceptionHandlerTest { } @Test - void 예상하지_못한_예외를_500_에러_응답으로_변환한다() { + @DisplayName("예상하지 못한 예외를 500 에러 응답으로 변환한다.") + void convertUnexpectedExceptionToInternalServerErrorResponse() { // given GlobalExceptionHandler globalExceptionHandler = new GlobalExceptionHandler(); Exception exception = new IllegalStateException(); From c0697abdb22c93ecaa7d926b52846f644bde42ef Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 14:42:40 +0900 Subject: [PATCH 42/43] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/domain/reservation/ReservationController.java | 2 +- .../domain/reservation/admin/AdminReservationController.java | 2 +- .../domain/reservation/ReservationControllerTest.java | 4 ++-- .../reservation/admin/AdminReservationControllerTest.java | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/roomescape/domain/reservation/ReservationController.java b/src/main/java/roomescape/domain/reservation/ReservationController.java index c572ceadc0..bd805a2a15 100644 --- a/src/main/java/roomescape/domain/reservation/ReservationController.java +++ b/src/main/java/roomescape/domain/reservation/ReservationController.java @@ -45,7 +45,7 @@ public ResponseEntity getUserReservations( } @DeleteMapping("/reservations/{id}") - public ResponseEntity deleteUserReservation( + public ResponseEntity cancelUserReservation( @PathVariable Long id ) { reservationService.cancelUserReservation(id); diff --git a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java index 519b3fedc4..756573515f 100644 --- a/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java +++ b/src/main/java/roomescape/domain/reservation/admin/AdminReservationController.java @@ -30,7 +30,7 @@ public ResponseEntity> getAllReservation(HttpServletRe } @DeleteMapping("/admin/reservations/{id}") - public ResponseEntity deleteReservation(HttpServletRequest request, @PathVariable Long id) { + public ResponseEntity cancelReservation(HttpServletRequest request, @PathVariable Long id) { if (validator.isUnauthorized(request)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } diff --git a/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java b/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java index 907ed2e382..c6e1ac2111 100644 --- a/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java +++ b/src/test/java/roomescape/domain/reservation/ReservationControllerTest.java @@ -141,7 +141,7 @@ void getUserReservationsWithoutName() throws Exception { @Test @DisplayName("예약 삭제의 정상 요청과 응답을 확인한다.") - void deleteUserReservation() throws Exception { + void cancelUserReservation() throws Exception { // given Long id = 1L; @@ -215,7 +215,7 @@ void updateReservationWhenReservationNotFound() throws Exception { @Test @DisplayName("예약 삭제 시 id를 누락한 경우 예외가 발생한다.") - void deleteUserReservationWithoutId() throws Exception { + void cancelUserReservationWithoutId() throws Exception { // given & when & then mockMvc.perform(delete("/reservations")) .andExpect(status().isMethodNotAllowed()); diff --git a/src/test/java/roomescape/domain/reservation/admin/AdminReservationControllerTest.java b/src/test/java/roomescape/domain/reservation/admin/AdminReservationControllerTest.java index 0fd1c738ed..d7e4c6e3e6 100644 --- a/src/test/java/roomescape/domain/reservation/admin/AdminReservationControllerTest.java +++ b/src/test/java/roomescape/domain/reservation/admin/AdminReservationControllerTest.java @@ -92,7 +92,7 @@ void getAllReservationWhenUnauthorized() throws Exception { @Test @DisplayName("관리자가 예약 삭제 시 요청과 응답을 확인한다.") - void deleteReservation() throws Exception { + void cancelReservation() throws Exception { // given Long id = 1L; when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(false); @@ -107,7 +107,7 @@ void deleteReservation() throws Exception { @Test @DisplayName("관리자 인증에 실패하면 예약 삭제 시 401을 반환한다.") - void deleteReservationWhenUnauthorized() throws Exception { + void cancelReservationWhenUnauthorized() throws Exception { // given Long id = 1L; when(validator.isUnauthorized(any(HttpServletRequest.class))).thenReturn(true); From 4f236dd7c9c7f3d32c5fe0b332cfe5f3e44e7dd5 Mon Sep 17 00:00:00 2001 From: Sumin Date: Mon, 18 May 2026 15:02:07 +0900 Subject: [PATCH 43/43] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20=EA=B3=A0=EC=9C=A0=ED=95=9C=20db=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20yml=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/application.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/test/resources/application.yml diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000000..ea24942fa4 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,5 @@ +spring: + datasource: + url: jdbc:h2:mem:${random.uuid};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + +token: ""