diff --git a/api/build.gradle b/api/build.gradle index 63e3dff8..edcb99fb 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,6 +1,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework:spring-tx:6.1.1' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' diff --git a/api/src/main/java/dev/hooon/booking/BookingApiController.java b/api/src/main/java/dev/hooon/booking/BookingApiController.java new file mode 100644 index 00000000..3fbba769 --- /dev/null +++ b/api/src/main/java/dev/hooon/booking/BookingApiController.java @@ -0,0 +1,39 @@ +package dev.hooon.booking; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import dev.hooon.auth.jwt.JwtAuthorization; +import dev.hooon.booking.application.BookingService; +import dev.hooon.booking.dto.response.BookingListResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class BookingApiController { + + private final BookingService bookingService; + + @GetMapping("/api/users/booking") + @Operation(summary = "예매 조회 API", description = "예매를 조회한다") + @ApiResponse(responseCode = "200", useReturnTypeSchema = true) + public ResponseEntity getBookings( + @Parameter(hidden = true) @JwtAuthorization Long userId, + @RequestParam(name = "duration") int duration, + @PageableDefault(size = 10) Pageable pageable + ) { + BookingListResponse bookingListResponse = bookingService.getBookings( + userId, + duration, + pageable + ); + return ResponseEntity.ok(bookingListResponse); + } +} diff --git a/api/src/test/java/dev/hooon/booking/BookingApiControllerTest.java b/api/src/test/java/dev/hooon/booking/BookingApiControllerTest.java new file mode 100644 index 00000000..782ccd92 --- /dev/null +++ b/api/src/test/java/dev/hooon/booking/BookingApiControllerTest.java @@ -0,0 +1,98 @@ +package dev.hooon.booking; + +import static org.springframework.http.HttpHeaders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import dev.hooon.common.support.ApiTestSupport; +import dev.hooon.user.domain.entity.User; + +@DisplayName("[BookingApiController API 테스트]") +@Sql("/sql/user_bookings_find.sql") +class BookingApiControllerTest extends ApiTestSupport { + + @Autowired + private MockMvc mockMvc; + + @DisplayName("사용자는 조회 기간에 따른 예매 정보를 조회할 수 있다 - 1") + @Test + void getBookings_test_1() throws Exception { + + // given + User user = new User(); + ReflectionTestUtils.setField(user, "id", 1L); + + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/users/booking?duration=3600&page=0&size=2") + .header(AUTHORIZATION, accessToken) + ); + + // then + resultActions.andExpectAll( + status().isOk(), + + jsonPath("$.bookingList.length()").value(2), + + jsonPath("$.bookingList[0].bookingId").isNumber(), + jsonPath("$.bookingList[0].bookingDate").isString(), + jsonPath("$.bookingList[0].showInfo.showName").isString(), + jsonPath("$.bookingList[0].showInfo.showDate").isString(), + jsonPath("$.bookingList[0].showInfo.showRound").isNumber(), + jsonPath("$.bookingList[0].showInfo.showRoundStartTime").isString(), + jsonPath("$.bookingList[0].ticketNumber").isNumber(), + jsonPath("$.bookingList[0].currentState").isString(), + + jsonPath("$.bookingList[1].bookingId").isNumber(), + jsonPath("$.bookingList[1].bookingDate").isString(), + jsonPath("$.bookingList[1].showInfo.showName").isString(), + jsonPath("$.bookingList[1].showInfo.showDate").isString(), + jsonPath("$.bookingList[1].showInfo.showRound").isNumber(), + jsonPath("$.bookingList[1].showInfo.showRoundStartTime").isString(), + jsonPath("$.bookingList[1].ticketNumber").isNumber(), + jsonPath("$.bookingList[1].currentState").isString() + ); + } + + @DisplayName("사용자는 조회 기간에 따른 예매 정보를 조회할 수 있다 - 2") + @Test + void getBookings_test_2() throws Exception { + + // given + User user = new User(); + ReflectionTestUtils.setField(user, "id", 1L); + + // when + ResultActions resultActions = mockMvc.perform( + MockMvcRequestBuilders + .get("/api/users/booking?duration=30&page=0&size=2") + .header(AUTHORIZATION, accessToken) + ); + + // then + resultActions.andExpectAll( + status().isOk(), + + jsonPath("$.bookingList.length()").value(1), + + jsonPath("$.bookingList[0].bookingId").isNumber(), + jsonPath("$.bookingList[0].bookingDate").isString(), + jsonPath("$.bookingList[0].showInfo.showName").isString(), + jsonPath("$.bookingList[0].showInfo.showDate").isString(), + jsonPath("$.bookingList[0].showInfo.showRound").isNumber(), + jsonPath("$.bookingList[0].showInfo.showRoundStartTime").isString(), + jsonPath("$.bookingList[0].ticketNumber").isNumber(), + jsonPath("$.bookingList[0].currentState").isString() + + ); + } +} diff --git a/core/src/main/java/dev/hooon/booking/application/BookingService.java b/core/src/main/java/dev/hooon/booking/application/BookingService.java index cbe07674..ff05e9df 100644 --- a/core/src/main/java/dev/hooon/booking/application/BookingService.java +++ b/core/src/main/java/dev/hooon/booking/application/BookingService.java @@ -2,7 +2,12 @@ import static dev.hooon.booking.exception.BookingErrorCode.*; +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import dev.hooon.booking.domain.entity.Booking; import dev.hooon.booking.domain.entity.BookingStatus; @@ -10,9 +15,9 @@ import dev.hooon.booking.domain.repository.BookingRepository; import dev.hooon.booking.dto.BookingMapper; import dev.hooon.booking.dto.response.BookingCancelResponse; +import dev.hooon.booking.dto.response.BookingListResponse; import dev.hooon.common.exception.NotFoundException; import dev.hooon.common.exception.ValidationException; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @Service @@ -65,4 +70,15 @@ public Booking getById(Long id) { ); } + @Transactional(readOnly = true) + public BookingListResponse getBookings( + Long userId, + int days, + Pageable pageable + ) { + LocalDateTime currentDateTime = LocalDateTime.now(); + LocalDateTime createdDateTime = currentDateTime.minusDays(days); + List bookingList = bookingRepository.findByUserIdAndDays(userId, createdDateTime, pageable); + return BookingMapper.toBookingListResponse(bookingList); + } } diff --git a/core/src/main/java/dev/hooon/booking/domain/entity/Booking.java b/core/src/main/java/dev/hooon/booking/domain/entity/Booking.java index 1b6f2a6d..a007a141 100644 --- a/core/src/main/java/dev/hooon/booking/domain/entity/Booking.java +++ b/core/src/main/java/dev/hooon/booking/domain/entity/Booking.java @@ -7,6 +7,9 @@ import static jakarta.persistence.GenerationType.*; import static lombok.AccessLevel.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.List; @@ -46,12 +49,12 @@ public class Booking extends TimeBaseEntity { @JoinColumn(name = "booking_show_id", nullable = false, foreignKey = @ForeignKey(value = NO_CONSTRAINT)) private Show show; - @Enumerated(STRING) - @Column(name = "booking_status", nullable = false) - private BookingStatus bookingStatus; + @Enumerated(STRING) + @Column(name = "booking_status", nullable = false) + private BookingStatus bookingStatus; - @Column(name = "booking_ticket_count", nullable = false) - private int ticketCount = 0; + @Column(name = "booking_ticket_count", nullable = false) + private int ticketCount = 0; @OneToMany(mappedBy = "booking", cascade = CascadeType.ALL, orphanRemoval = true) private final List tickets = new ArrayList<>(); @@ -68,6 +71,13 @@ private Booking(User user, Show show) { this.bookingStatus = BOOKED; } + private Booking(User user, Show show, LocalDateTime localDateTime) { + this.user = user; + this.show = show; + this.bookingStatus = BOOKED; + this.createdAt = localDateTime; + } + public static Booking of( User user, Show show @@ -75,6 +85,14 @@ public static Booking of( return new Booking(user, show); } + public static Booking of( + User user, + Show show, + LocalDateTime localDateTime + ) { + return new Booking(user, show, localDateTime); + } + public void markBookingStatusAsCanceled() { this.bookingStatus = CANCELED; } @@ -82,4 +100,25 @@ public void markBookingStatusAsCanceled() { public long getUserId() { return this.user.getId(); } + + public String getShowName() { + return this.getShow().getName(); + } + + public LocalDate getShowDate() { + return getFirstTicket().getShowDate(); + } + + public int getRound() { + return getFirstTicket().getRound(); + } + + public LocalTime getStartTime() { + return getFirstTicket().getStartTime(); + } + + public Ticket getFirstTicket() { + return this.getTickets().get(0); + } + } diff --git a/core/src/main/java/dev/hooon/booking/domain/entity/Ticket.java b/core/src/main/java/dev/hooon/booking/domain/entity/Ticket.java index 6fc716ad..0e077f36 100644 --- a/core/src/main/java/dev/hooon/booking/domain/entity/Ticket.java +++ b/core/src/main/java/dev/hooon/booking/domain/entity/Ticket.java @@ -5,6 +5,9 @@ import static jakarta.persistence.GenerationType.*; import static lombok.AccessLevel.*; +import java.time.LocalDate; +import java.time.LocalTime; + import dev.hooon.common.entity.TimeBaseEntity; import dev.hooon.show.domain.entity.seat.Seat; import jakarta.persistence.Column; @@ -54,4 +57,16 @@ public void setBooking(Booking booking) { public void markSeatStatusAsCanceled() { this.seat.markSeatStatusAsCanceled(); } + + public LocalDate getShowDate() { + return this.getSeat().getShowDate(); + } + + public int getRound() { + return this.getSeat().getRound(); + } + + public LocalTime getStartTime() { + return this.getSeat().getStartTime(); + } } diff --git a/core/src/main/java/dev/hooon/booking/domain/repository/BookingRepository.java b/core/src/main/java/dev/hooon/booking/domain/repository/BookingRepository.java index abf1078d..64229c0b 100644 --- a/core/src/main/java/dev/hooon/booking/domain/repository/BookingRepository.java +++ b/core/src/main/java/dev/hooon/booking/domain/repository/BookingRepository.java @@ -1,8 +1,11 @@ package dev.hooon.booking.domain.repository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Pageable; + import dev.hooon.booking.domain.entity.Booking; public interface BookingRepository { @@ -12,4 +15,10 @@ public interface BookingRepository { List findAll(); Optional findByIdWithTickets(Long id); + + List findByUserIdAndDays( + Long userId, + LocalDateTime createdDateTime, + Pageable pageable + ); } diff --git a/core/src/main/java/dev/hooon/booking/dto/BookingMapper.java b/core/src/main/java/dev/hooon/booking/dto/BookingMapper.java index 49a92852..da5ff28c 100644 --- a/core/src/main/java/dev/hooon/booking/dto/BookingMapper.java +++ b/core/src/main/java/dev/hooon/booking/dto/BookingMapper.java @@ -4,6 +4,9 @@ import dev.hooon.booking.domain.entity.Booking; import dev.hooon.booking.dto.response.BookingCancelResponse; +import dev.hooon.booking.dto.response.BookingListResponse; +import dev.hooon.booking.dto.response.BookingResponse; +import dev.hooon.booking.dto.response.ShowInfoResponse; import dev.hooon.booking.dto.response.TicketBookingResponse; import dev.hooon.booking.dto.response.TicketResponse; import dev.hooon.booking.dto.response.TicketSeatResponse; @@ -45,4 +48,22 @@ public static BookingCancelResponse toBookingCancelResponse(Booking booking) { ticketSeatResponseList ); } + + public static BookingListResponse toBookingListResponse(List bookingList) { + List bookingResponseList = bookingList.stream() + .map(booking -> new BookingResponse( + booking.getId(), + booking.getCreatedAt().toLocalDate(), + new ShowInfoResponse( + booking.getShowName(), + booking.getShowDate(), + booking.getRound(), + booking.getStartTime() + ), + booking.getTicketCount(), + booking.getBookingStatus().toString() + )) + .toList(); + return new BookingListResponse(bookingResponseList); + } } diff --git a/core/src/main/java/dev/hooon/booking/dto/response/BookingListResponse.java b/core/src/main/java/dev/hooon/booking/dto/response/BookingListResponse.java new file mode 100644 index 00000000..e256ba78 --- /dev/null +++ b/core/src/main/java/dev/hooon/booking/dto/response/BookingListResponse.java @@ -0,0 +1,8 @@ +package dev.hooon.booking.dto.response; + +import java.util.List; + +public record BookingListResponse( + List bookingList +) { +} diff --git a/core/src/main/java/dev/hooon/booking/dto/response/BookingResponse.java b/core/src/main/java/dev/hooon/booking/dto/response/BookingResponse.java new file mode 100644 index 00000000..c9a8a954 --- /dev/null +++ b/core/src/main/java/dev/hooon/booking/dto/response/BookingResponse.java @@ -0,0 +1,15 @@ +package dev.hooon.booking.dto.response; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonFormat; + +public record BookingResponse( + long bookingId, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate bookingDate, + ShowInfoResponse showInfo, + int ticketNumber, + String currentState +) { +} diff --git a/core/src/main/java/dev/hooon/booking/dto/response/ShowInfoResponse.java b/core/src/main/java/dev/hooon/booking/dto/response/ShowInfoResponse.java new file mode 100644 index 00000000..c978a2e3 --- /dev/null +++ b/core/src/main/java/dev/hooon/booking/dto/response/ShowInfoResponse.java @@ -0,0 +1,16 @@ +package dev.hooon.booking.dto.response; + +import java.time.LocalDate; +import java.time.LocalTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +public record ShowInfoResponse( + String showName, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate showDate, + int showRound, + @JsonFormat(pattern = "HH:mm:ss") + LocalTime showRoundStartTime +) { +} diff --git a/core/src/main/java/dev/hooon/booking/infrastructure/adaptor/BookingRepositoryAdaptor.java b/core/src/main/java/dev/hooon/booking/infrastructure/adaptor/BookingRepositoryAdaptor.java index c39a7794..90ce653f 100644 --- a/core/src/main/java/dev/hooon/booking/infrastructure/adaptor/BookingRepositoryAdaptor.java +++ b/core/src/main/java/dev/hooon/booking/infrastructure/adaptor/BookingRepositoryAdaptor.java @@ -1,8 +1,11 @@ package dev.hooon.booking.infrastructure.adaptor; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import dev.hooon.booking.domain.entity.Booking; @@ -30,4 +33,18 @@ public List findAll() { public Optional findByIdWithTickets(Long id) { return bookingJpaRepository.findByIdWithTickets(id); } + + @Override + public List findByUserIdAndDays( + Long userId, + LocalDateTime createdDateTime, + Pageable pageable + ) { + Page bookingPage = bookingJpaRepository.findBookingsByUserIdAndCreatedAtAfter( + userId, + createdDateTime, + pageable + ); + return bookingPage.getContent(); + } } diff --git a/core/src/main/java/dev/hooon/booking/infrastructure/repository/BookingJpaRepository.java b/core/src/main/java/dev/hooon/booking/infrastructure/repository/BookingJpaRepository.java index da3d2600..46b86fae 100644 --- a/core/src/main/java/dev/hooon/booking/infrastructure/repository/BookingJpaRepository.java +++ b/core/src/main/java/dev/hooon/booking/infrastructure/repository/BookingJpaRepository.java @@ -1,7 +1,10 @@ package dev.hooon.booking.infrastructure.repository; +import java.time.LocalDateTime; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -12,4 +15,12 @@ public interface BookingJpaRepository extends JpaRepository { @Query("SELECT DISTINCT b FROM Booking b LEFT JOIN FETCH b.tickets WHERE b.id = :bookingId") Optional findByIdWithTickets(@Param("bookingId") Long bookingId); + + @Query("SELECT b FROM Booking b WHERE b.user.id = :userId AND b.createdAt >= :createdDateTime") + Page findBookingsByUserIdAndCreatedAtAfter( + @Param("userId") Long userId, + @Param("createdDateTime") LocalDateTime createdDateTime, + Pageable pageable + ); + } diff --git a/core/src/testFixtures/resources/sql/user_bookings_find.sql b/core/src/testFixtures/resources/sql/user_bookings_find.sql new file mode 100644 index 00000000..f38b03ea --- /dev/null +++ b/core/src/testFixtures/resources/sql/user_bookings_find.sql @@ -0,0 +1,76 @@ +INSERT INTO place_table + (place_id, place_name, place_contact_info, place_address, place_url) +VALUES (1, '블루스퀘어 신한카드홀', '1544-1591', '서울특별시 용산구 이태원로 294 블루스퀘어(한남동)', 'http://www.bluesquare.kr/index.asp'); +-- +-- +INSERT INTO show_table +(show_id, show_name, show_category, show_start_date, show_end_date, show_total_minutes, show_intermission_minutes, + show_age_limit, show_total_seats, show_place_id) +VALUES (1, '레미제라블', 'MUSICAL', '2024-01-01', '2024-12-31', 150, 15, '만 8세 이상', 1000, 1); +-- +-- +INSERT INTO show_table +(show_id, show_name, show_category, show_start_date, show_end_date, show_total_minutes, show_intermission_minutes, + show_age_limit, show_total_seats, show_place_id) +VALUES (2, '오페라의 유령', 'MUSICAL', '2024-01-01', '2024-12-31', 150, 15, '만 8세 이상', 1000, 1); +-- +-- +INSERT INTO seat_table +(seat_id, seat_show_id, seat_grade, seat_is_seat, seat_sector, seat_row, seat_col, seat_price, seat_show_date, + seat_show_round, seat_start_time, seat_status) +VALUES (1, 1, 'VIP', true, '1층', 'A', 2, 100000, '2024-01-01', 2, '13:00:00', 'AVAILABLE'); +-- +-- +INSERT INTO seat_table +(seat_id, seat_show_id, seat_grade, seat_is_seat, seat_sector, seat_row, seat_col, seat_price, seat_show_date, + seat_show_round, seat_start_time, seat_status) +VALUES (2, 1, 'VIP', true, '1층', 'A', 3, 100000, '2024-01-01', 2, '13:00:00', 'AVAILABLE'); +-- +-- +INSERT INTO seat_table +(seat_id, seat_show_id, seat_grade, seat_is_seat, seat_sector, seat_row, seat_col, seat_price, seat_show_date, + seat_show_round, seat_start_time, seat_status) +VALUES (3, 1, 'S', true, '2층', 'A', 2, 70000, '2024-01-01', 2, '13:00:00', 'AVAILABLE'); +-- +-- +INSERT INTO seat_table +(seat_id, seat_show_id, seat_grade, seat_is_seat, seat_sector, seat_row, seat_col, seat_price, seat_show_date, + seat_show_round, seat_start_time, seat_status) +VALUES (4, 2, 'VIP', true, '1층', 'A', 2, 100000, '2024-01-01', 2, '13:00:00', 'AVAILABLE'); +-- +-- +INSERT INTO seat_table +(seat_id, seat_show_id, seat_grade, seat_is_seat, seat_sector, seat_row, seat_col, seat_price, seat_show_date, + seat_show_round, seat_start_time, seat_status) +VALUES (5, 2, 'VIP', true, '1층', 'A', 3, 100000, '2024-01-01', 2, '13:00:00', 'AVAILABLE'); +-- +-- +INSERT INTO booking_table +(booking_id, booking_user_id, booking_show_id, booking_status, booking_ticket_count, created_at) +VALUES (1, 1, 1, 'BOOKED', 3, '2024-01-01'); +-- +-- +INSERT INTO ticket_table + (ticket_id, ticket_seat_id, booking_id) +VALUES (1, 1, 1); +-- +INSERT INTO ticket_table + (ticket_id, ticket_seat_id, booking_id) +VALUES (2, 2, 1); +-- +INSERT INTO ticket_table + (ticket_id, ticket_seat_id, booking_id) +VALUES (3, 3, 1); + +-- +INSERT INTO booking_table +(booking_id, booking_user_id, booking_show_id, booking_status, booking_ticket_count, created_at) +VALUES (2, 1, 2, 'BOOKED', 2, '2023-01-01'); +-- +INSERT INTO ticket_table + (ticket_id, ticket_seat_id, booking_id) +VALUES (4, 4, 2); +-- +INSERT INTO ticket_table + (ticket_id, ticket_seat_id, booking_id) +VALUES (5, 5, 2);