From 2bda61c646b9f9f9c59bccdd3eafaeccd9aab3d4 Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 00:59:06 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=20Concert=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20=EC=98=88=EC=95=BD=20=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?=EA=B3=B5=EC=97=B0=20=EC=9D=BC=EC=A0=95=20&=20=EA=B3=B5?= =?UTF-8?q?=EC=97=B0=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=95=A9=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concert/model/ConcertScheduleWithConcert.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/slam/concertreservation/domain/concert/model/ConcertScheduleWithConcert.java diff --git a/src/main/java/com/slam/concertreservation/domain/concert/model/ConcertScheduleWithConcert.java b/src/main/java/com/slam/concertreservation/domain/concert/model/ConcertScheduleWithConcert.java new file mode 100644 index 0000000..11fd928 --- /dev/null +++ b/src/main/java/com/slam/concertreservation/domain/concert/model/ConcertScheduleWithConcert.java @@ -0,0 +1,11 @@ +package com.slam.concertreservation.domain.concert.model; + +/** + * 예약 가능 공연 일정 조회 시 반환되는 공연 일정 & 공연 정보 조합 모델 + * @param concertSchedule + * @param concert + */ +public record ConcertScheduleWithConcert( + ConcertSchedule concertSchedule, + Concert concert +) {} From 1732d9e65e625c9695dd47ff395a3652434597c4 Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 01:04:36 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=20Concert=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20JpaRepository=20=EA=B0=80=20=EC=A0=9C=EA=B3=B5?= =?UTF-8?q?=ED=95=98=EB=8A=94=20findAllById()=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=EB=A5=BC=20ConcertRepository=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/concert/repository/ConcertRepository.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/slam/concertreservation/domain/concert/repository/ConcertRepository.java b/src/main/java/com/slam/concertreservation/domain/concert/repository/ConcertRepository.java index 4659001..1cb22c2 100644 --- a/src/main/java/com/slam/concertreservation/domain/concert/repository/ConcertRepository.java +++ b/src/main/java/com/slam/concertreservation/domain/concert/repository/ConcertRepository.java @@ -1,6 +1,7 @@ package com.slam.concertreservation.domain.concert.repository; import com.slam.concertreservation.domain.concert.model.Concert; +import java.util.List; import java.util.Optional; public interface ConcertRepository { @@ -8,4 +9,6 @@ public interface ConcertRepository { Concert save(Concert concert); Optional findById(Long id); + + List findAllById(List ids); } From ea60cb95adab50be9ce345dd005eb65256564a1e Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 01:14:58 +0900 Subject: [PATCH 3/9] =?UTF-8?q?=20Concert=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20List=20->=20List=20=EC=A1=B0=ED=95=A9=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=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 --- .../concert/service/ConcertService.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java b/src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java index c3eb5bb..562ee2a 100644 --- a/src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java +++ b/src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java @@ -3,6 +3,7 @@ import com.slam.concertreservation.common.error.ErrorCode; import com.slam.concertreservation.domain.concert.model.Concert; import com.slam.concertreservation.domain.concert.model.ConcertSchedule; +import com.slam.concertreservation.domain.concert.model.ConcertScheduleWithConcert; import com.slam.concertreservation.domain.concert.model.Seat; import com.slam.concertreservation.domain.concert.repository.ConcertRepository; import com.slam.concertreservation.domain.concert.repository.ConcertScheduleRepository; @@ -11,6 +12,7 @@ import com.slam.concertreservation.infrastructure.persistence.redis.locking.RedissonDistributedLock; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -265,14 +267,32 @@ public List getAvailableSeatsOfConcertSchedule(Long concertScheduleId) { return availableSeats; } - /** - * 예약 가능 공연 일정 목록 조회. - * - * @param presentDateTime - * @return - */ - public List getAvailableConcertSchedule(LocalDateTime presentDateTime) { - return concertScheduleRepository.findAllAvailable(presentDateTime); + + @Transactional(readOnly = true) + public List getAvailableConcertScheduleWithConcert(LocalDateTime presentDateTime) { + + // 예약 가능한 공연 일정 조회 + List schedules = concertScheduleRepository.findAllAvailable(presentDateTime); + + // 예약 가능한 공연 ID 목록 추출 + List concertIds = schedules.stream() + .map(ConcertSchedule::getConcertId) + .distinct() + .toList(); + + // 공연 ID 목록으로 공연 조회 + Map concerts = concertRepository.findAllById(concertIds) + .stream() + .collect( + java.util.stream.Collectors.toMap(Concert::getId, concert -> concert)); + + // ConcertSchedule과 Concert를 결합하여 ConcertScheduleWithConcert 생성 + return schedules.stream() + .map(schedule -> new ConcertScheduleWithConcert( + schedule, + concerts.get(schedule.getConcertId()) + )) + .toList(); } /** From 98a836a31a3adc7709654937157396989944a368 Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 01:19:44 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=20Concer=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20Facade=20method=20=EC=9D=98=20return=20type=20-?= =?UTF-8?q?=20List=20->=20List=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/facade/ConcertReservationApplication.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/slam/concertreservation/application/facade/ConcertReservationApplication.java b/src/main/java/com/slam/concertreservation/application/facade/ConcertReservationApplication.java index 0d51dd0..38787a8 100644 --- a/src/main/java/com/slam/concertreservation/application/facade/ConcertReservationApplication.java +++ b/src/main/java/com/slam/concertreservation/application/facade/ConcertReservationApplication.java @@ -2,6 +2,7 @@ import com.slam.concertreservation.domain.concert.model.Concert; import com.slam.concertreservation.domain.concert.model.ConcertSchedule; +import com.slam.concertreservation.domain.concert.model.ConcertScheduleWithConcert; import com.slam.concertreservation.domain.concert.model.Seat; import com.slam.concertreservation.domain.concert.service.ConcertService; import com.slam.concertreservation.domain.point.model.PointHistory; @@ -116,8 +117,8 @@ public ConcertSchedule getConcertSchedule(Long concertScheduleId) { * * @return */ - public List getAvailableConcertSchedules() { - return concertService.getAvailableConcertSchedule(LocalDateTime.now()); + public List getAvailableConcertSchedulesWithConcert() { + return concertService.getAvailableConcertScheduleWithConcert(LocalDateTime.now()); } /** From 972fefff7c1d90a919c5105e0c457853ef3a4b62 Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 01:22:46 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=20Concert=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20Controller=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/ConcertReservationController.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java b/src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java index bb5d43e..62a17ae 100644 --- a/src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java +++ b/src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java @@ -5,6 +5,7 @@ import com.slam.concertreservation.component.idempotency.Idempotent; import com.slam.concertreservation.domain.concert.model.Concert; import com.slam.concertreservation.domain.concert.model.ConcertSchedule; +import com.slam.concertreservation.domain.concert.model.ConcertScheduleWithConcert; import com.slam.concertreservation.domain.concert.model.Seat; import com.slam.concertreservation.domain.point.model.UserPointBalance; import com.slam.concertreservation.domain.queue.model.Token; @@ -118,14 +119,11 @@ public ResponseEntity registerConcertSchedule( */ @GetMapping("/concerts/schedules/available") public ResponseEntity> getAvailableConcertSchedules() { - List schedules = reservationApp.getAvailableConcertSchedules(); - List responses = schedules.stream() - .map(schedule -> { - Concert concert = reservationApp.getConcert(schedule.getConcertId()); - return ConcertScheduleResponse.from(schedule, concert); - }) - .toList(); - return ResponseEntity.ok(responses); + List schedules = reservationApp.getAvailableConcertSchedulesWithConcert(); + + return ResponseEntity.ok(schedules.stream() + .map(r -> ConcertScheduleResponse.from(r.concertSchedule(), r.concert())) + .toList()); } /** From cb8bcda7336f78bb8dc775da04de13c7afd05de2 Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 01:32:44 +0900 Subject: [PATCH 6/9] =?UTF-8?q?=20Concert=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20RepositoryImpl=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/impl/ConcertRepositoryImpl.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/slam/concertreservation/infrastructure/persistence/jpa/impl/ConcertRepositoryImpl.java b/src/main/java/com/slam/concertreservation/infrastructure/persistence/jpa/impl/ConcertRepositoryImpl.java index 08032ab..8ede4a8 100644 --- a/src/main/java/com/slam/concertreservation/infrastructure/persistence/jpa/impl/ConcertRepositoryImpl.java +++ b/src/main/java/com/slam/concertreservation/infrastructure/persistence/jpa/impl/ConcertRepositoryImpl.java @@ -4,6 +4,7 @@ import com.slam.concertreservation.domain.concert.repository.ConcertRepository; import com.slam.concertreservation.infrastructure.persistence.jpa.ConcertJpaRepository; import com.slam.concertreservation.infrastructure.persistence.jpa.entities.ConcertJpaEntity; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -25,4 +26,12 @@ public Optional findById(Long concertId) { return concertJpaRepository.findById(concertId) .map(ConcertJpaEntity::toDomain); } + + @Override + public List findAllById(List ids) { + return concertJpaRepository.findAllById(ids) + .stream() + .map(ConcertJpaEntity::toDomain) + .toList(); + } } From f04c27a4f4802acabe97f89cd1a3b1f91a6a4039 Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 01:33:29 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=20Concert=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20ConcertScheduleWithConcert=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=20=EB=8F=84=EC=9E=85=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95=20-=20100%=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rentConcertReservationIntegrationTest.java | 7 +++-- .../service/ConcertServiceUnitTest.java | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java b/src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java index 283c0d3..f712d20 100644 --- a/src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java +++ b/src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java @@ -4,6 +4,7 @@ import com.slam.concertreservation.application.facade.ConcertReservationApplication; import com.slam.concertreservation.application.facade.UserApplication; import com.slam.concertreservation.domain.concert.model.ConcertSchedule; +import com.slam.concertreservation.domain.concert.model.ConcertScheduleWithConcert; import com.slam.concertreservation.domain.concert.model.Seat; import com.slam.concertreservation.domain.concert.model.SeatStatus; import com.slam.concertreservation.domain.concert.repository.ConcertRepository; @@ -177,12 +178,14 @@ void shouldSuccessfullyGetAvailableConcertSchedule() { 1000); // when - List actual = concertReservationApplication.getAvailableConcertSchedules(); + List actual = concertReservationApplication + .getAvailableConcertSchedulesWithConcert(); // then Assertions.assertNotNull(actual); // Verify that the schedule we just created is present in the results - boolean isPresent = actual.stream().anyMatch(cs -> cs.getId().equals(expected.getId())); + boolean isPresent = actual.stream() + .anyMatch(cs -> cs.concertSchedule().getId().equals(expected.getId())); Assertions.assertTrue(isPresent, "Expected schedule should be available"); } diff --git a/src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java b/src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java index 05e28e8..eded14d 100644 --- a/src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java +++ b/src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java @@ -3,6 +3,7 @@ import com.slam.concertreservation.common.error.ErrorCode; import com.slam.concertreservation.domain.concert.model.Concert; import com.slam.concertreservation.domain.concert.model.ConcertSchedule; +import com.slam.concertreservation.domain.concert.model.ConcertScheduleWithConcert; import com.slam.concertreservation.domain.concert.model.Seat; import com.slam.concertreservation.domain.concert.model.SeatStatus; import com.slam.concertreservation.domain.concert.repository.ConcertRepository; @@ -17,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import java.util.List; import java.util.Optional; import org.mockito.MockitoAnnotations; import org.springframework.context.ApplicationEventPublisher; @@ -321,4 +323,31 @@ void shouldThrowIllegalStateException_WhenSeatAlreadyUnassigned() { .isInstanceOf(BusinessRuleViolationException.class); } } + + @Nested + class GetAvailableConcertScheduleWithConcertUnitTest { + @Test + @DisplayName("성공 : 예약 가능한 공연 일정과 공연 정보를 함께 조회한다.") + void shouldReturnConcertSchedulesWithConcert_WhenAvailableSchedulesExist() { + // given + LocalDateTime now = LocalDateTime.now(); + Concert concert = Concert.create("OASIS REUNION", "oasis"); + ConcertSchedule schedule = ConcertSchedule.create(concert.getId(), now.plusDays(1), now, + now.plusDays(1)); + + when(concertScheduleRepository.findAllAvailable(any(LocalDateTime.class))) + .thenReturn(List.of(schedule)); + when(concertRepository.findAllById(anyList())) + .thenReturn(List.of(concert)); + + // when + List actual = concertService + .getAvailableConcertScheduleWithConcert(now); + + // then + assertEquals(1, actual.size()); + assertEquals(schedule, actual.get(0).concertSchedule()); + assertEquals(concert, actual.get(0).concert()); + } + } } \ No newline at end of file From 272f0baf1bc0f7324441f534c69daefac1731266 Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 01:54:05 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=20Concert=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20NPE-safe=20=ED=95=98=EB=8F=84=EB=A1=9D=20filter?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concert/service/ConcertService.java | 5 ++-- .../service/ConcertServiceUnitTest.java | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java b/src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java index 562ee2a..00ee566 100644 --- a/src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java +++ b/src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java @@ -267,7 +267,6 @@ public List getAvailableSeatsOfConcertSchedule(Long concertScheduleId) { return availableSeats; } - @Transactional(readOnly = true) public List getAvailableConcertScheduleWithConcert(LocalDateTime presentDateTime) { @@ -288,10 +287,10 @@ public List getAvailableConcertScheduleWithConcert(L // ConcertSchedule과 Concert를 결합하여 ConcertScheduleWithConcert 생성 return schedules.stream() + .filter(schedule -> concerts.get(schedule.getConcertId()) != null) .map(schedule -> new ConcertScheduleWithConcert( schedule, - concerts.get(schedule.getConcertId()) - )) + concerts.get(schedule.getConcertId()))) .toList(); } diff --git a/src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java b/src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java index eded14d..c5bc6be 100644 --- a/src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java +++ b/src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java @@ -349,5 +349,33 @@ void shouldReturnConcertSchedulesWithConcert_WhenAvailableSchedulesExist() { assertEquals(schedule, actual.get(0).concertSchedule()); assertEquals(concert, actual.get(0).concert()); } + + @Test + @DisplayName("성공 : 공연 정보가 없는 공연 일정은 조회 결과에서 제외된다.") + void shouldFilterOutSchedules_WhenConcertNotFound() { + // given + LocalDateTime now = LocalDateTime.now(); + Concert concert = Concert.create("OASIS REUNION", "oasis"); + ConcertSchedule schedule1 = ConcertSchedule.create(concert.getId(), now.plusDays(1), now, + now.plusDays(1)); + ConcertSchedule schedule2 = ConcertSchedule.create(2L, now.plusDays(1), now, now.plusDays(1)); // Concert + // not + // found + + when(concertScheduleRepository.findAllAvailable(any(LocalDateTime.class))) + .thenReturn(List.of(schedule1, schedule2)); + when(concertRepository.findAllById(anyList())) + .thenReturn(List.of(concert)); + + // when + List actual = concertService + .getAvailableConcertScheduleWithConcert(now); + + // then + assertEquals(1, actual.size()); + assertEquals(schedule1, actual.get(0).concertSchedule()); + assertEquals(concert, actual.get(0).concert()); + } } + } \ No newline at end of file From 15b226c7d9927652539a9d388acaba77499b7b7d Mon Sep 17 00:00:00 2001 From: Ho Yeon CHAE Date: Mon, 9 Feb 2026 02:03:43 +0900 Subject: [PATCH 9/9] =?UTF-8?q?=20Concert=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20:=20NPE-safety=20=EC=9C=84=ED=95=9C=20filter=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC,=20Test=20=EC=8B=9C=20Concert=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EB=8F=84=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=ED=95=98=EC=97=AC=20ID=20=EC=A1=B0=ED=9A=8C=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rentConcertReservationIntegrationTest.java | 383 +++++++++--------- 1 file changed, 199 insertions(+), 184 deletions(-) diff --git a/src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java b/src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java index f712d20..2981a4a 100644 --- a/src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java +++ b/src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java @@ -3,6 +3,7 @@ import com.slam.concertreservation.application.event.listener.PaymentEventListener; import com.slam.concertreservation.application.facade.ConcertReservationApplication; import com.slam.concertreservation.application.facade.UserApplication; +import com.slam.concertreservation.domain.concert.model.Concert; import com.slam.concertreservation.domain.concert.model.ConcertSchedule; import com.slam.concertreservation.domain.concert.model.ConcertScheduleWithConcert; import com.slam.concertreservation.domain.concert.model.Seat; @@ -37,209 +38,223 @@ @SpringBootTest public class NonConcurrentConcertReservationIntegrationTest { - private static final Logger log = LoggerFactory.getLogger(NonConcurrentConcertReservationIntegrationTest.class); + private static final Logger log = LoggerFactory.getLogger(NonConcurrentConcertReservationIntegrationTest.class); - @Autowired - private ConcertReservationApplication concertReservationApplication; + @Autowired + private ConcertReservationApplication concertReservationApplication; - @Autowired - private PaymentEventListener paymentEventListener; + @Autowired + private PaymentEventListener paymentEventListener; - @Autowired - private UserApplication userApplication; + @Autowired + private UserApplication userApplication; - @Autowired - private ConcertRepository concertRepository; + @Autowired + private ConcertRepository concertRepository; - @Autowired - private ReservationRepository reservationRepository; + @Autowired + private ReservationRepository reservationRepository; - @Autowired - private ConcertScheduleRepository concertScheduleRepository; + @Autowired + private ConcertScheduleRepository concertScheduleRepository; - @Autowired - private UserRepository userRepository; + @Autowired + private UserRepository userRepository; - @Autowired - private TokenRepository tokenRepository; + @Autowired + private TokenRepository tokenRepository; - @Autowired - private SeatRepository seatRepository; + @Autowired + private SeatRepository seatRepository; - @Autowired - private PointHistoryRepository pointHistoryRepository; + @Autowired + private PointHistoryRepository pointHistoryRepository; - @Autowired - private UserPointBalanceRepository userPointBalanceRepository; + @Autowired + private UserPointBalanceRepository userPointBalanceRepository; - String userName = "userName"; - String concertName = "concertName"; - String artistName = "oasis"; - LocalDateTime concertDateTime = LocalDateTime.now().plusDays(15); // Concert in 15 days - LocalDateTime reservationStartAt = LocalDateTime.now(); // Reservation starts now - LocalDateTime reservationEndAt = LocalDateTime.now().plusDays(14); // Reservation ends 1 day before concert - ConcertSchedule concertSchedule = ConcertSchedule.create(1L, concertDateTime, reservationStartAt, - reservationEndAt); + String userName = "userName"; + String concertName = "concertName"; + String artistName = "oasis"; + LocalDateTime concertDateTime = LocalDateTime.now().plusDays(15); // Concert in 15 days + LocalDateTime reservationStartAt = LocalDateTime.now(); // Reservation starts now + LocalDateTime reservationEndAt = LocalDateTime.now().plusDays(14); // Reservation ends 1 day before concert + ConcertSchedule concertSchedule = ConcertSchedule.create(1L, concertDateTime, reservationStartAt, + reservationEndAt); - @Autowired - private PointService pointService; - @Autowired - private ReservationService reservationService; + @Autowired + private PointService pointService; + @Autowired + private ReservationService reservationService; - @Nested - class UserIntegrationTest { - @Test - @DisplayName("성공 : 회원 가입 시 회원 조회가 가능하다.") - void shouldFindUser_WhenUserSignedUp() { - // given - String userName = "한성경"; + @Nested + class UserIntegrationTest { + @Test + @DisplayName("성공 : 회원 가입 시 회원 조회가 가능하다.") + void shouldFindUser_WhenUserSignedUp() { + // given + String userName = "한성경"; - // when - User createdUser = userApplication.registerUser(userName); - User foundUser = userApplication.getUser(createdUser.getId()); + // when + User createdUser = userApplication.registerUser(userName); + User foundUser = userApplication.getUser(createdUser.getId()); - // then - Assertions.assertEquals(createdUser.getId(), foundUser.getId()); + // then + Assertions.assertEquals(createdUser.getId(), foundUser.getId()); + } } - } - - @Nested - class UserPointIntegrationTest { - - @Test - @DisplayName("성공 : 회원 가입 시 해당 회원의 포인트 잔액 0인 UserPointBalance가 생성되어 저장된다. 이때 INIT 타입의 PointHistory도 생성되어 저장된다.") - void shouldCreateAndSaveUserPointBalanceWith0AndInitPointHistory_WhenUserSignedUp() { - // given - String userName = "한성경"; - - // when - User createdUser = userApplication.registerUser(userName); - UserPointBalance actualUserPointBalance = concertReservationApplication - .getUserPointBalance(createdUser.getId()); - List actualPointHistory = concertReservationApplication - .getPointHistories(createdUser.getId()); - - // then - Assertions.assertEquals(0, actualUserPointBalance.balance().getAmount()); - Assertions.assertEquals(1, actualPointHistory.size()); - Assertions.assertEquals(0, actualPointHistory.get(0).transactionAmount()); - Assertions.assertEquals(PointTransactionType.INIT, actualPointHistory.get(0).transactionType()); - } - - @Test - @DisplayName("성공 : 회원의 포인트 잔액을 증액한다. 이때 PointHistory가 생성되어 저장된다.") - void shouldIncreaseUserPointBalanceAndCreatePointHistoryOfCharge_whenUserChargesPoint() { - // given - User user = userApplication.registerUser(userName); - - // when - UserPointBalance actualUserPointBalance = concertReservationApplication.chargeUserPoint(user.getId(), 1000); - List actualPointHistory = concertReservationApplication.getPointHistories(user.getId()); - - // then - Assertions.assertNotNull(actualUserPointBalance); - Assertions.assertEquals(1000, actualUserPointBalance.balance().getAmount()); - - Assertions.assertNotNull(actualPointHistory); - Assertions.assertEquals(2, actualPointHistory.size()); - } - - @Test - @DisplayName("성공 : 회원의 포인트 잔액을 감소한다. 이때 USE 타입의 PointHistory가 생성되어 저장된다.") - void shouldDecreaseUserPointBalanceAndCreatePointHistoryOfUse_whenUserUsesPoint() { - // given - User user = userApplication.registerUser("정종환"); - concertReservationApplication.chargeUserPoint(user.getId(), 1000); - - // when - UserPointBalance actualUserPointBalance = concertReservationApplication.useUserPoint(user.getId(), 500); - PointHistory expectedPointHistory = PointHistory.create(user.getId(), PointTransactionType.USE, 500); - List actualPointHistory = concertReservationApplication.getPointHistories(user.getId()); - - // then - Assertions.assertNotNull(actualUserPointBalance); - Assertions.assertEquals(500, actualUserPointBalance.balance().getAmount()); - - Assertions.assertNotNull(actualPointHistory); - Assertions.assertEquals(3, actualPointHistory.size()); - } - } - - @Nested - class ConcertReservationIntegrationTest { - - @Test - @DisplayName("성공 : 예약 가능한 공연 일정을 조회한다.") - void shouldSuccessfullyGetAvailableConcertSchedule() { - // given - // Create an available schedule (start time <= now) - ConcertSchedule expected = concertReservationApplication.registerConcertSchedule(1L, - concertDateTime.plusDays(1), LocalDateTime.now().minusMinutes(1), reservationEndAt.plusDays(1), - 1000); - - // when - List actual = concertReservationApplication - .getAvailableConcertSchedulesWithConcert(); - - // then - Assertions.assertNotNull(actual); - // Verify that the schedule we just created is present in the results - boolean isPresent = actual.stream() - .anyMatch(cs -> cs.concertSchedule().getId().equals(expected.getId())); - Assertions.assertTrue(isPresent, "Expected schedule should be available"); - } - - @Test - @DisplayName("성공 : 특정 공연 일정의 예약 가능 좌석 목록을 조회한다.") - void shouldSuccessfullyGetAvailableSeats() { - // given - ConcertSchedule concertSchedule = concertReservationApplication.registerConcertSchedule(1L, - concertDateTime, reservationStartAt, reservationEndAt, 1000); - - // when - List actual = concertReservationApplication.getAvailableSeats(concertSchedule.getId()); - // then - Assertions.assertNotNull(actual); - Assertions.assertEquals(Seat.MAX_SEAT_NUMBER, actual.size()); + @Nested + class UserPointIntegrationTest { + + @Test + @DisplayName("성공 : 회원 가입 시 해당 회원의 포인트 잔액 0인 UserPointBalance가 생성되어 저장된다. 이때 INIT 타입의 PointHistory도 생성되어 저장된다.") + void shouldCreateAndSaveUserPointBalanceWith0AndInitPointHistory_WhenUserSignedUp() { + // given + String userName = "한성경"; + + // when + User createdUser = userApplication.registerUser(userName); + UserPointBalance actualUserPointBalance = concertReservationApplication + .getUserPointBalance(createdUser.getId()); + List actualPointHistory = concertReservationApplication + .getPointHistories(createdUser.getId()); + + // then + Assertions.assertEquals(0, actualUserPointBalance.balance().getAmount()); + Assertions.assertEquals(1, actualPointHistory.size()); + Assertions.assertEquals(0, actualPointHistory.get(0).transactionAmount()); + Assertions.assertEquals(PointTransactionType.INIT, actualPointHistory.get(0).transactionType()); + } + + @Test + @DisplayName("성공 : 회원의 포인트 잔액을 증액한다. 이때 PointHistory가 생성되어 저장된다.") + void shouldIncreaseUserPointBalanceAndCreatePointHistoryOfCharge_whenUserChargesPoint() { + // given + User user = userApplication.registerUser(userName); + + // when + UserPointBalance actualUserPointBalance = concertReservationApplication + .chargeUserPoint(user.getId(), 1000); + List actualPointHistory = concertReservationApplication + .getPointHistories(user.getId()); + + // then + Assertions.assertNotNull(actualUserPointBalance); + Assertions.assertEquals(1000, actualUserPointBalance.balance().getAmount()); + + Assertions.assertNotNull(actualPointHistory); + Assertions.assertEquals(2, actualPointHistory.size()); + } + + @Test + @DisplayName("성공 : 회원의 포인트 잔액을 감소한다. 이때 USE 타입의 PointHistory가 생성되어 저장된다.") + void shouldDecreaseUserPointBalanceAndCreatePointHistoryOfUse_whenUserUsesPoint() { + // given + User user = userApplication.registerUser("정종환"); + concertReservationApplication.chargeUserPoint(user.getId(), 1000); + + // when + UserPointBalance actualUserPointBalance = concertReservationApplication + .useUserPoint(user.getId(), 500); + PointHistory expectedPointHistory = PointHistory.create(user.getId(), PointTransactionType.USE, + 500); + List actualPointHistory = concertReservationApplication + .getPointHistories(user.getId()); + + // then + Assertions.assertNotNull(actualUserPointBalance); + Assertions.assertEquals(500, actualUserPointBalance.balance().getAmount()); + + Assertions.assertNotNull(actualPointHistory); + Assertions.assertEquals(3, actualPointHistory.size()); + } } - @Test - @DisplayName("성공 : 특정 공연 일정의 특정 좌석을 예약하고 결제한다. 예약 후 해당 좌석 조회 시 상태는 UNAVAILABLE 이다. 또한 예약 상태는 PAID 이다. 잔액은 차감된다.") - void shouldSuccessfullyConfirmAndPaySeat() throws InterruptedException { - // given - User user = userApplication.registerUser(userName); - concertReservationApplication.chargeUserPoint(user.getId(), 2000); - - ConcertSchedule concertSchedule = concertReservationApplication.registerConcertSchedule(1L, - concertDateTime, reservationStartAt, reservationEndAt, 1000); - Seat seat = concertReservationApplication.getAvailableSeats(concertSchedule.getId()).get(0); - - // when - Seat assignedSeat = concertReservationApplication.assignSeat(concertSchedule.getId(), user.getId(), - seat.getId()); // 좌석 선점 - Reservation createdTemporaryReservation = concertReservationApplication.createTemporaryReservation( - user.getId(), concertSchedule.getId(), assignedSeat.getId(), assignedSeat.getPrice()); // 가예약 생성 - - concertReservationApplication.paymentRequestForReservation(user.getId(), seat.getPrice(), - createdTemporaryReservation.getId()); // 결제 요청 - - UserPointBalance updatedUserPointBalance = concertReservationApplication.getUserPointBalance(user.getId()); // 차감된 - // 사용자 - // 잔액 - Seat reservedSeat = concertReservationApplication.getSeat(seat.getId()); // 예약된 좌석 - Reservation confirmedReservation = concertReservationApplication - .getReservation(createdTemporaryReservation.getId()); // 확정된 예약 - log.warn("결제 완료 후 예약 확정되어 저장된 Reservation 상태 : {}", confirmedReservation.getStatus()); - log.warn("결제 완료 후 예약 확정되어 저장된 Reservation ID : {}", confirmedReservation.getId()); - - Thread.sleep(1000); - - // then - Assertions.assertEquals(updatedUserPointBalance.balance().getAmount(), 1000); - Assertions.assertEquals(SeatStatus.UNAVAILABLE, reservedSeat.getStatus()); - Assertions.assertEquals(ReservationStatus.CONFIRMED, confirmedReservation.getStatus()); - Assertions.assertEquals(1000, updatedUserPointBalance.balance().getAmount()); + @Nested + class ConcertReservationIntegrationTest { + + @Test + @DisplayName("성공 : 예약 가능한 공연 일정을 조회한다.") + void shouldSuccessfullyGetAvailableConcertSchedule() { + // given + // Create a concert first to ensure it exists for the schedule + Concert concert = concertReservationApplication.registerConcert("Test Concert", "Test Artist"); + + // Create an available schedule (start time <= now) + ConcertSchedule expected = concertReservationApplication.registerConcertSchedule( + concert.getId(), + concertDateTime.plusDays(1), LocalDateTime.now().minusMinutes(1), + reservationEndAt.plusDays(1), + 1000); + + // when + List actual = concertReservationApplication + .getAvailableConcertSchedulesWithConcert(); + + // then + Assertions.assertNotNull(actual); + // Verify that the schedule we just created is present in the results + boolean isPresent = actual.stream() + .anyMatch(cs -> cs.concertSchedule().getId().equals(expected.getId())); + Assertions.assertTrue(isPresent, "Expected schedule should be available"); + } + + @Test + @DisplayName("성공 : 특정 공연 일정의 예약 가능 좌석 목록을 조회한다.") + void shouldSuccessfullyGetAvailableSeats() { + // given + ConcertSchedule concertSchedule = concertReservationApplication.registerConcertSchedule(1L, + concertDateTime, reservationStartAt, reservationEndAt, 1000); + + // when + List actual = concertReservationApplication.getAvailableSeats(concertSchedule.getId()); + + // then + Assertions.assertNotNull(actual); + Assertions.assertEquals(Seat.MAX_SEAT_NUMBER, actual.size()); + } + + @Test + @DisplayName("성공 : 특정 공연 일정의 특정 좌석을 예약하고 결제한다. 예약 후 해당 좌석 조회 시 상태는 UNAVAILABLE 이다. 또한 예약 상태는 PAID 이다. 잔액은 차감된다.") + void shouldSuccessfullyConfirmAndPaySeat() throws InterruptedException { + // given + User user = userApplication.registerUser(userName); + concertReservationApplication.chargeUserPoint(user.getId(), 2000); + + ConcertSchedule concertSchedule = concertReservationApplication.registerConcertSchedule(1L, + concertDateTime, reservationStartAt, reservationEndAt, 1000); + Seat seat = concertReservationApplication.getAvailableSeats(concertSchedule.getId()).get(0); + + // when + Seat assignedSeat = concertReservationApplication.assignSeat(concertSchedule.getId(), + user.getId(), + seat.getId()); // 좌석 선점 + Reservation createdTemporaryReservation = concertReservationApplication + .createTemporaryReservation( + user.getId(), concertSchedule.getId(), assignedSeat.getId(), + assignedSeat.getPrice()); // 가예약 생성 + + concertReservationApplication.paymentRequestForReservation(user.getId(), seat.getPrice(), + createdTemporaryReservation.getId()); // 결제 요청 + + UserPointBalance updatedUserPointBalance = concertReservationApplication + .getUserPointBalance(user.getId()); // 차감된 + // 사용자 + // 잔액 + Seat reservedSeat = concertReservationApplication.getSeat(seat.getId()); // 예약된 좌석 + Reservation confirmedReservation = concertReservationApplication + .getReservation(createdTemporaryReservation.getId()); // 확정된 예약 + log.warn("결제 완료 후 예약 확정되어 저장된 Reservation 상태 : {}", confirmedReservation.getStatus()); + log.warn("결제 완료 후 예약 확정되어 저장된 Reservation ID : {}", confirmedReservation.getId()); + + Thread.sleep(1000); + + // then + Assertions.assertEquals(updatedUserPointBalance.balance().getAmount(), 1000); + Assertions.assertEquals(SeatStatus.UNAVAILABLE, reservedSeat.getStatus()); + Assertions.assertEquals(ReservationStatus.CONFIRMED, confirmedReservation.getStatus()); + Assertions.assertEquals(1000, updatedUserPointBalance.balance().getAmount()); + } } - } }