From 04c36acc4088f303242b6944cc2a3cc1da3af71b Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 25 Sep 2025 16:42:01 +0900 Subject: [PATCH 01/16] add show detail retrieval feature with API documentation --- docs/specs/api/show_detail.md | 85 +++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/specs/api/show_detail.md diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md new file mode 100644 index 0000000..5095e86 --- /dev/null +++ b/docs/specs/api/show_detail.md @@ -0,0 +1,85 @@ +# 기능 명세서: 공연 상세 조회 + +## 개요 + +공연의 상세 정보를 조회하는 기능이다. +공연 기본 정보(제목, 유형, 등급, 기간, 시놉시스, 포스터, 공연장)와 함께 등록된 회차 정보를 확인할 수 있다. +공연 목록 조회 후 특정 공연을 선택했을 때 상세 페이지로 진입하기 위한 핵심 엔드포인트이다. + +## Endpoint + +- Method: `GET` +- URL: `/api/show/{showId}` + +## 요청 파라미터 + +- Path Variable + - `showId` (Long, required): 조회할 공연의 고유 식별자 + +## 요청 예시 + +GET /api/show/1 + +## 응답 본문 + +```json +{ + "status": "SUCCESS", + "data": { + "contents": [ + { + "showId": 1, + "title": "라라랜드", + "type": "MUSICAL", + "rating": "ALL", + "synopsis": "꿈을 좇는 두 청춘의 사랑과 음악 이야기", + "posterUrl": "https://example.com/posters/lalaland.jpg", + "hallName": "샤롯데씨어터", + "performanceStartDate": "2025-10-05", + "performanceEndDate": "2025-11-05", + "schedules": [ + { + "scheduleId": 10, + "startAt": "2025-10-10T19:00:00", + "endAt": "2025-10-10T21:30:00", + "runtimeMinutes": 150 + }, + { + "scheduleId": 11, + "startAt": "2025-10-11T14:00:00", + "endAt": "2025-10-11T16:30:00", + "runtimeMinutes": 150 + } + ] + } + ], + "page": 0, + "size": 1, + "hasNext": false + }, + "timestamp": "2025-09-25T00:00:00Z" +} +``` + +응답 코드 + +- 200 OK: 정상적으로 조회된 경우 +- 400 BAD_REQUEST: 잘못된 showId 값이 전달된 경우 +- 404 NOT_FOUND: 존재하지 않는 공연을 조회한 경우 + +## Policy + +- 공연 기간(performanceStartDate ≤ performanceEndDate)은 등록 시점에 검증되므로 조회 시 항상 유효한 값을 반환한다. +- schedules는 공연에 연결된 회차가 없으면 빈 배열을 반환한다. + +테스트 시나리오 + +- [] 존재하는 showId를 요청하면 200과 함께 공연 상세 정보가 반환된다 +- [] 존재하지 않는 showId 요청 시 NOT_FOUND를 반환한다 +- [] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 +- [] 공연에 회차가 없는 경우 schedules는 빈 배열이다 +- [] 존재하는 공연장 ID가 조회된다 +- [] 공연 일정은 마감 이전의 일정만 조회된다 +- [] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 +- [] hall 정보는 hallId와 hallName을 모두 포함한다 +- [] schedules는 startAt ASC, 이름 순으로 정렬되어 반환된다 From 72b76bebb0c0a21e2baa687483fda0f3cfacbcdd Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 25 Sep 2025 23:36:15 +0900 Subject: [PATCH 02/16] add show detail inquiry endpoint and response model --- ...AuthorizationRequestMatcherConfigurer.java | 1 + .../adapter/webapi/ShowController.java | 21 ++++++ .../mandarin/booking/utils/TestFixture.java | 19 +++++ .../booking/webapi/show/showId/GET_specs.java | 32 +++++++++ docs/specs/api/show_detail.md | 72 +++++++++---------- .../domain/show/ShowDetailResponse.java | 41 +++++++++++ 6 files changed, 149 insertions(+), 37 deletions(-) create mode 100644 application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java create mode 100644 domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/ApplicationAuthorizationRequestMatcherConfigurer.java b/application/src/main/java/org/mandarin/booking/adapter/security/ApplicationAuthorizationRequestMatcherConfigurer.java index 9e0c056..2b12f8f 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/security/ApplicationAuthorizationRequestMatcherConfigurer.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/ApplicationAuthorizationRequestMatcherConfigurer.java @@ -18,6 +18,7 @@ public void authorizeRequests( .requestMatchers("/api/auth/reissue").permitAll() .requestMatchers(HttpMethod.POST, "/api/show/schedule").hasAuthority("ROLE_DISTRIBUTOR") .requestMatchers(HttpMethod.GET, "/api/show").permitAll() + .requestMatchers(HttpMethod.GET, "/api/show/*").permitAll()// 인증이 필요한 GET /show/* 엔드포인트 추가시 설정을 이 줄 아래에 .requestMatchers(HttpMethod.POST, "/api/show").hasAuthority("ROLE_ADMIN") .anyRequest().authenticated(); } diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java index a1df9aa..4f7ff14 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java @@ -1,9 +1,14 @@ package org.mandarin.booking.adapter.webapi; +import static org.mandarin.booking.domain.show.ShowDetailResponse.HallResponse; + import jakarta.validation.Valid; +import java.time.LocalDate; +import java.util.List; import org.mandarin.booking.adapter.SliceView; import org.mandarin.booking.app.show.ShowFetcher; import org.mandarin.booking.app.show.ShowRegisterer; +import org.mandarin.booking.domain.show.ShowDetailResponse; import org.mandarin.booking.domain.show.ShowInquiryRequest; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.show.ShowRegisterResponse; @@ -25,6 +30,22 @@ SliceView inquire(@Valid ShowInquiryRequest req) { return showFetcher.fetchShows(req.page(), req.size(), req.type(), req.rating(), req.q(), req.from(), req.to()); } + @GetMapping("/{showId}") + ShowDetailResponse inquireDetail(Long showId) { + return new ShowDetailResponse( + showId, + "title", + "type", + "rating", + "synopsis", + "posterUrl", + LocalDate.now(), + LocalDate.now(), + new HallResponse(1L, "hallName"), + List.of() + ); + } + @PostMapping ShowRegisterResponse register(@RequestBody @Valid ShowRegisterRequest request) { return showRegisterer.register(request); diff --git a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java index b23c7f2..b6e3d93 100644 --- a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java +++ b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java @@ -8,6 +8,7 @@ import jakarta.persistence.EntityManager; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.Random; import java.util.UUID; @@ -22,6 +23,7 @@ import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.Show.Type; import org.mandarin.booking.domain.show.ShowRegisterRequest; +import org.mandarin.booking.domain.show.ShowScheduleCreateCommand; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.annotation.Transactional; @@ -89,6 +91,23 @@ public Hall insertDummyHall() { return hall; } + public Show generateShow(int scheduleCount) { + var hall = insertDummyHall(); + var show = generateShow(hall.getId()); + + IntStream.range(0, scheduleCount).forEach(i -> { + Random random = new Random(); + var startAt = LocalDateTime.now().plusDays(random.nextInt(0, 10)); + var command = new ShowScheduleCreateCommand(show.getId(), + startAt, + startAt.plusHours(random.nextInt(2, 5)) + ); + show.registerSchedule(command); + }); + + return showInsert(show); + } + public List generateShows(int showCount) { var hall = insertDummyHall(); return IntStream.range(0, showCount) diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java new file mode 100644 index 0000000..60972ad --- /dev/null +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -0,0 +1,32 @@ +package org.mandarin.booking.webapi.show.showId; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mandarin.booking.domain.show.ShowResponse; +import org.mandarin.booking.utils.IntegrationTest; +import org.mandarin.booking.utils.IntegrationTestUtils; +import org.mandarin.booking.utils.TestFixture; +import org.springframework.beans.factory.annotation.Autowired; + +@IntegrationTest +@DisplayName("GET /api/show/{showId}") +class GET_specs { + @Test + void 존재하는_showId를_요청하면_200과_함께_공연_상세_정보가_반환된다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + + // Act + var response = testUtils.get("/api/show/" + show.getId()) + .assertSuccess(ShowResponse.class); + + // Assert + assertThat(response.getStatus()).isEqualTo(SUCCESS); + } +} diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 5095e86..cad56ee 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -23,42 +23,40 @@ GET /api/show/1 ## 응답 본문 ```json + { "status": "SUCCESS", "data": { - "contents": [ + "showId": 1, + "title": "라라랜드", + "type": "MUSICAL", + "rating": "ALL", + "synopsis": "꿈을 좇는 두 청춘의 사랑과 음악 이야기", + "posterUrl": "https://cdn.example.com/posters/la_la_land.jpg", + "performanceStartDate": "2025-10-05", + "performanceEndDate": "2025-11-05", + "hall": { + "hallId": 3, + "hallName": "샤롯데씨어터" + }, + "schedules": [ + { + "scheduleId": 10, + "startAt": "2025-10-10T19:00:00", + "endAt": "2025-10-10T21:30:00", + "runtimeMinutes": 150 + }, { - "showId": 1, - "title": "라라랜드", - "type": "MUSICAL", - "rating": "ALL", - "synopsis": "꿈을 좇는 두 청춘의 사랑과 음악 이야기", - "posterUrl": "https://example.com/posters/lalaland.jpg", - "hallName": "샤롯데씨어터", - "performanceStartDate": "2025-10-05", - "performanceEndDate": "2025-11-05", - "schedules": [ - { - "scheduleId": 10, - "startAt": "2025-10-10T19:00:00", - "endAt": "2025-10-10T21:30:00", - "runtimeMinutes": 150 - }, - { - "scheduleId": 11, - "startAt": "2025-10-11T14:00:00", - "endAt": "2025-10-11T16:30:00", - "runtimeMinutes": 150 - } - ] + "scheduleId": 11, + "startAt": "2025-10-11T14:00:00", + "endAt": "2025-10-11T16:30:00", + "runtimeMinutes": 150 } - ], - "page": 0, - "size": 1, - "hasNext": false + ] }, "timestamp": "2025-09-25T00:00:00Z" } + ``` 응답 코드 @@ -74,12 +72,12 @@ GET /api/show/1 테스트 시나리오 -- [] 존재하는 showId를 요청하면 200과 함께 공연 상세 정보가 반환된다 -- [] 존재하지 않는 showId 요청 시 NOT_FOUND를 반환한다 -- [] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 -- [] 공연에 회차가 없는 경우 schedules는 빈 배열이다 -- [] 존재하는 공연장 ID가 조회된다 -- [] 공연 일정은 마감 이전의 일정만 조회된다 -- [] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 -- [] hall 정보는 hallId와 hallName을 모두 포함한다 -- [] schedules는 startAt ASC, 이름 순으로 정렬되어 반환된다 +- [x] 존재하는 showId를 요청하면 200과 함께 공연 상세 정보가 반환된다 +- [ ] 존재하지 않는 showId 요청 시 NOT_FOUND를 반환한다 +- [ ] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 +- [ ] 공연에 회차가 없는 경우 schedules는 빈 배열이다 +- [ ] 존재하는 공연장 ID가 조회된다 +- [ ] 공연 일정은 마감 이전의 일정만 조회된다 +- [ ] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 +- [ ] hall 정보는 hallId와 hallName을 모두 포함한다 +- [ ] schedules는 startAt ASC, 이름 순으로 정렬되어 반환된다 diff --git a/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java new file mode 100644 index 0000000..8edc0fb --- /dev/null +++ b/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java @@ -0,0 +1,41 @@ +package org.mandarin.booking.domain.show; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Getter; + +public record ShowDetailResponse( + Long showId, + String title, + String type, + String rating, + String synopsis, + String posterUrl, + LocalDate performanceStartDate, + LocalDate performanceEndDate, + HallResponse hall, + List schedules +) { + public record HallResponse( + Long hallId, + String hallName + ) { + } + + @Getter + public static final class ShowScheduleResponse { + private final Long scheduleId; + private final LocalDateTime startAt; + private final LocalDateTime endAt; + private final long runtimeMinutes; + + public ShowScheduleResponse(Long scheduleId, LocalDateTime startAt, LocalDateTime endAt) { + this.scheduleId = scheduleId; + this.startAt = startAt; + this.endAt = endAt; + this.runtimeMinutes = Duration.between(startAt, endAt).toMinutes(); + } + } +} From f2052e7545c4f9992e33ff244ca4fd64406aa3c8 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 25 Sep 2025 23:40:18 +0900 Subject: [PATCH 03/16] add show detail retrieval for non-existent showId with NOT_FOUND response --- .../adapter/webapi/ShowController.java | 20 +++---------------- .../booking/app/show/ShowFetcher.java | 3 +++ .../booking/app/show/ShowService.java | 7 +++++++ .../booking/webapi/show/showId/GET_specs.java | 18 +++++++++++++++++ docs/specs/api/show_detail.md | 2 +- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java index 4f7ff14..963d648 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java @@ -1,10 +1,6 @@ package org.mandarin.booking.adapter.webapi; -import static org.mandarin.booking.domain.show.ShowDetailResponse.HallResponse; - import jakarta.validation.Valid; -import java.time.LocalDate; -import java.util.List; import org.mandarin.booking.adapter.SliceView; import org.mandarin.booking.app.show.ShowFetcher; import org.mandarin.booking.app.show.ShowRegisterer; @@ -16,6 +12,7 @@ import org.mandarin.booking.domain.show.ShowScheduleRegisterRequest; import org.mandarin.booking.domain.show.ShowScheduleRegisterResponse; 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.RequestMapping; @@ -31,19 +28,8 @@ SliceView inquire(@Valid ShowInquiryRequest req) { } @GetMapping("/{showId}") - ShowDetailResponse inquireDetail(Long showId) { - return new ShowDetailResponse( - showId, - "title", - "type", - "rating", - "synopsis", - "posterUrl", - LocalDate.now(), - LocalDate.now(), - new HallResponse(1L, "hallName"), - List.of() - ); + ShowDetailResponse inquireDetail(@PathVariable Long showId) { + return showFetcher.fetchShowDetail(showId); } @PostMapping diff --git a/application/src/main/java/org/mandarin/booking/app/show/ShowFetcher.java b/application/src/main/java/org/mandarin/booking/app/show/ShowFetcher.java index 89da51c..3d48798 100644 --- a/application/src/main/java/org/mandarin/booking/app/show/ShowFetcher.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowFetcher.java @@ -2,9 +2,12 @@ import java.time.LocalDate; import org.mandarin.booking.adapter.SliceView; +import org.mandarin.booking.domain.show.ShowDetailResponse; import org.mandarin.booking.domain.show.ShowResponse; public interface ShowFetcher { SliceView fetchShows(Integer page, Integer size, String type, String rating, String q, LocalDate from, LocalDate to); + + ShowDetailResponse fetchShowDetail(Long showId); } diff --git a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java index 6113b80..b7fcffd 100644 --- a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java @@ -11,6 +11,7 @@ import org.mandarin.booking.domain.show.Show.Rating; import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.Show.Type; +import org.mandarin.booking.domain.show.ShowDetailResponse; import org.mandarin.booking.domain.show.ShowException; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.show.ShowRegisterResponse; @@ -61,6 +62,12 @@ public SliceView fetchShows(Integer page, Integer size, String typ q, from, to); } + @Override + public ShowDetailResponse fetchShowDetail(Long showId) { + var show = queryRepository.findById(showId); + return null; + } + private void checkDuplicateTitle(String title) { if (queryRepository.existsByName(title)) { throw new ShowException("이미 존재하는 공연 이름입니다:" + title); diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 60972ad..55a6540 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -1,6 +1,7 @@ package org.mandarin.booking.webapi.show.showId; import static org.assertj.core.api.Assertions.assertThat; +import static org.mandarin.booking.adapter.ApiStatus.NOT_FOUND; import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; import org.junit.jupiter.api.DisplayName; @@ -29,4 +30,21 @@ class GET_specs { // Assert assertThat(response.getStatus()).isEqualTo(SUCCESS); } + + @Test + void 존재하지_않는_showId_요청_시_NOT_FOUND를_반환한다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + var invalidShowId = show.getId() + 9999; + + // Act + var response = testUtils.get("/api/show/" + invalidShowId) + .assertFailure(); + + // Assert + assertThat(response.getStatus()).isEqualTo(NOT_FOUND); + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index cad56ee..3979546 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -73,7 +73,7 @@ GET /api/show/1 테스트 시나리오 - [x] 존재하는 showId를 요청하면 200과 함께 공연 상세 정보가 반환된다 -- [ ] 존재하지 않는 showId 요청 시 NOT_FOUND를 반환한다 +- [x] 존재하지 않는 showId 요청 시 NOT_FOUND를 반환한다 - [ ] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 - [ ] 공연에 회차가 없는 경우 schedules는 빈 배열이다 - [ ] 존재하는 공연장 ID가 조회된다 From 209e0b0d3582d1bfa79707a5b0155ac1326f8a91 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 00:06:02 +0900 Subject: [PATCH 04/16] add validation for showId to return BAD_REQUEST for non-positive values --- .../booking/adapter/webapi/ShowController.java | 3 ++- .../booking/webapi/show/showId/GET_specs.java | 17 +++++++++++++++++ docs/specs/api/show_detail.md | 2 +- .../booking/adapter/GlobalExceptionHandler.java | 6 ++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java index 963d648..67a21f3 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java @@ -1,6 +1,7 @@ package org.mandarin.booking.adapter.webapi; import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import org.mandarin.booking.adapter.SliceView; import org.mandarin.booking.app.show.ShowFetcher; import org.mandarin.booking.app.show.ShowRegisterer; @@ -28,7 +29,7 @@ SliceView inquire(@Valid ShowInquiryRequest req) { } @GetMapping("/{showId}") - ShowDetailResponse inquireDetail(@PathVariable Long showId) { + ShowDetailResponse inquireDetail(@PathVariable @Positive(message = "show Id는 음수일 수 없습니다.") Long showId) { return showFetcher.fetchShowDetail(showId); } diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 55a6540..8fec4d6 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -1,6 +1,7 @@ package org.mandarin.booking.webapi.show.showId; import static org.assertj.core.api.Assertions.assertThat; +import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.ApiStatus.NOT_FOUND; import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; @@ -47,4 +48,20 @@ class GET_specs { // Assert assertThat(response.getStatus()).isEqualTo(NOT_FOUND); } + + @Test + void 양의_정수가_아닌_showId_요청_시_BAD_REQUEST을_반환한다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + + // Act + var response = testUtils.get("/api/show/" + -show.getId())// 음수 + .assertFailure(); + + // Assert + assertThat(response.getStatus()).isEqualTo(BAD_REQUEST); + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 3979546..34db946 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -74,7 +74,7 @@ GET /api/show/1 - [x] 존재하는 showId를 요청하면 200과 함께 공연 상세 정보가 반환된다 - [x] 존재하지 않는 showId 요청 시 NOT_FOUND를 반환한다 -- [ ] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 +- [x] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 - [ ] 공연에 회차가 없는 경우 schedules는 빈 배열이다 - [ ] 존재하는 공연장 ID가 조회된다 - [ ] 공연 일정은 마감 이전의 일정만 조회된다 diff --git a/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java b/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java index 52f4860..c90f79d 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.servlet.NoHandlerFoundException; @Slf4j @@ -39,4 +40,9 @@ public ErrorResponse handleValidationException(MethodArgumentNotValidException e public ErrorResponse handleNoHandlerFoundException(NoHandlerFoundException ex) { return new ErrorResponse(NOT_FOUND, ex.getMessage()); } + + @ExceptionHandler(HandlerMethodValidationException.class) + public ErrorResponse handleHandlerMethodValidationException(HandlerMethodValidationException ex) { + return new ErrorResponse(BAD_REQUEST, ex.getMessage()); + } } From dc05f77bbcea042c963217cda8643a4fb0d68f12 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 00:13:01 +0900 Subject: [PATCH 05/16] add parameterized tests for invalid showId and enhance error handling for type mismatches --- .../booking/adapter/webapi/ShowController.java | 4 +++- .../mandarin/booking/webapi/show/showId/GET_specs.java | 10 +++++++--- docs/specs/api/show_detail.md | 2 +- .../booking/adapter/GlobalExceptionHandler.java | 6 ++++++ .../org/mandarin/booking/adapter/SecurityConfig.java | 9 +++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java index 67a21f3..a56d74d 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java @@ -29,7 +29,9 @@ SliceView inquire(@Valid ShowInquiryRequest req) { } @GetMapping("/{showId}") - ShowDetailResponse inquireDetail(@PathVariable @Positive(message = "show Id는 음수일 수 없습니다.") Long showId) { + ShowDetailResponse inquireDetail(@PathVariable + @Positive(message = "show Id는 음수일 수 없습니다.") + Long showId) { return showFetcher.fetchShowDetail(showId); } diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 8fec4d6..7626f50 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mandarin.booking.domain.show.ShowResponse; import org.mandarin.booking.utils.IntegrationTest; import org.mandarin.booking.utils.IntegrationTestUtils; @@ -49,16 +51,18 @@ class GET_specs { assertThat(response.getStatus()).isEqualTo(NOT_FOUND); } - @Test + @ParameterizedTest + @ValueSource(strings = {"0", "abc", "1.5", "@#$%", "-10"}) void 양의_정수가_아닌_showId_요청_시_BAD_REQUEST을_반환한다( + String invalidShowId, @Autowired IntegrationTestUtils testUtils, @Autowired TestFixture testFixture ) { // Arrange - var show = testFixture.generateShow(5); + testFixture.generateShow(5); // Act - var response = testUtils.get("/api/show/" + -show.getId())// 음수 + var response = testUtils.get("/api/show/" + invalidShowId) .assertFailure(); // Assert diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 34db946..62a7bcc 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -75,7 +75,7 @@ GET /api/show/1 - [x] 존재하는 showId를 요청하면 200과 함께 공연 상세 정보가 반환된다 - [x] 존재하지 않는 showId 요청 시 NOT_FOUND를 반환한다 - [x] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 -- [ ] 공연에 회차가 없는 경우 schedules는 빈 배열이다 +- [x] 공연에 회차가 없는 경우 schedules는 빈 배열이다 - [ ] 존재하는 공연장 ID가 조회된다 - [ ] 공연 일정은 마감 이전의 일정만 조회된다 - [ ] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 diff --git a/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java b/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java index c90f79d..d632e25 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.HandlerMethodValidationException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.NoHandlerFoundException; @Slf4j @@ -45,4 +46,9 @@ public ErrorResponse handleNoHandlerFoundException(NoHandlerFoundException ex) { public ErrorResponse handleHandlerMethodValidationException(HandlerMethodValidationException ex) { return new ErrorResponse(BAD_REQUEST, ex.getMessage()); } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ErrorResponse handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) { + return new ErrorResponse(BAD_REQUEST, ex.getMessage()); + } } diff --git a/internal/src/main/java/org/mandarin/booking/adapter/SecurityConfig.java b/internal/src/main/java/org/mandarin/booking/adapter/SecurityConfig.java index cbaf9ea..f6cd9a5 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/SecurityConfig.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/SecurityConfig.java @@ -17,6 +17,8 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.StrictHttpFirewall; @Configuration @RequiredArgsConstructor @@ -51,6 +53,13 @@ public SecurityFilterChain apiChain(HttpSecurity http, return http.build(); } + @Bean + public HttpFirewall allowUrlEncodedPercentHttpFirewall() { + StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowUrlEncodedPercent(true); + return firewall; + } + @Bean static RoleHierarchy roleHierarchy() { return RoleHierarchyImpl.withDefaultRolePrefix() From 7751cb66da7db83c33ef239ffebde5afc35be42f Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 15:57:07 +0900 Subject: [PATCH 06/16] add show detail retrieval for existing hall ID with successful response --- .../booking/app/hall/HallFetcher.java | 7 ++++++ .../booking/app/hall/HallQueryRepository.java | 7 ++++++ .../booking/app/hall/HallRepository.java | 5 ++-- .../booking/app/hall/HallService.java | 8 ++++++- .../booking/app/show/ShowService.java | 17 ++++++++++++- .../mandarin/booking/utils/TestFixture.java | 6 +++++ .../booking/webapi/show/showId/GET_specs.java | 24 +++++++++++++++++++ docs/specs/api/show_detail.md | 2 +- 8 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 application/src/main/java/org/mandarin/booking/app/hall/HallFetcher.java diff --git a/application/src/main/java/org/mandarin/booking/app/hall/HallFetcher.java b/application/src/main/java/org/mandarin/booking/app/hall/HallFetcher.java new file mode 100644 index 0000000..7527b13 --- /dev/null +++ b/application/src/main/java/org/mandarin/booking/app/hall/HallFetcher.java @@ -0,0 +1,7 @@ +package org.mandarin.booking.app.hall; + +import org.mandarin.booking.domain.hall.Hall; + +public interface HallFetcher { + Hall fetch(Long hallId); +} diff --git a/application/src/main/java/org/mandarin/booking/app/hall/HallQueryRepository.java b/application/src/main/java/org/mandarin/booking/app/hall/HallQueryRepository.java index 7eedc66..382defd 100644 --- a/application/src/main/java/org/mandarin/booking/app/hall/HallQueryRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/hall/HallQueryRepository.java @@ -1,6 +1,8 @@ package org.mandarin.booking.app.hall; import lombok.RequiredArgsConstructor; +import org.mandarin.booking.domain.hall.Hall; +import org.mandarin.booking.domain.hall.HallException; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -13,4 +15,9 @@ class HallQueryRepository { boolean existsById(Long hallId) { return repository.existsById(hallId); } + + public Hall findById(Long hallId) { + return repository.findById(hallId) + .orElseThrow(() -> new HallException("NOT_FOUND", "해당 공연장을 찾을 수 없습니다.")); + } } diff --git a/application/src/main/java/org/mandarin/booking/app/hall/HallRepository.java b/application/src/main/java/org/mandarin/booking/app/hall/HallRepository.java index 66a25c5..49f6f53 100644 --- a/application/src/main/java/org/mandarin/booking/app/hall/HallRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/hall/HallRepository.java @@ -1,10 +1,11 @@ package org.mandarin.booking.app.hall; +import java.util.Optional; import org.mandarin.booking.domain.hall.Hall; import org.springframework.data.repository.Repository; interface HallRepository extends Repository { - Hall save(Hall hall); - boolean existsById(Long id); + + Optional findById(Long id); } diff --git a/application/src/main/java/org/mandarin/booking/app/hall/HallService.java b/application/src/main/java/org/mandarin/booking/app/hall/HallService.java index ce32723..9c0063a 100644 --- a/application/src/main/java/org/mandarin/booking/app/hall/HallService.java +++ b/application/src/main/java/org/mandarin/booking/app/hall/HallService.java @@ -1,12 +1,13 @@ package org.mandarin.booking.app.hall; import lombok.RequiredArgsConstructor; +import org.mandarin.booking.domain.hall.Hall; import org.mandarin.booking.domain.hall.HallException; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -class HallService implements HallValidator { +class HallService implements HallValidator, HallFetcher { private final HallQueryRepository queryRepository; @Override @@ -15,4 +16,9 @@ public void checkHallExist(Long hallId) { throw new HallException("NOT_FOUND", "해당 공연장을 찾을 수 없습니다."); } } + + @Override + public Hall fetch(Long hallId) { + return queryRepository.findById(hallId); + } } diff --git a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java index b7fcffd..c0d5ead 100644 --- a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java @@ -6,12 +6,14 @@ import java.time.LocalDate; import lombok.RequiredArgsConstructor; import org.mandarin.booking.adapter.SliceView; +import org.mandarin.booking.app.hall.HallFetcher; import org.mandarin.booking.app.hall.HallValidator; import org.mandarin.booking.domain.show.Show; import org.mandarin.booking.domain.show.Show.Rating; import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.Show.Type; import org.mandarin.booking.domain.show.ShowDetailResponse; +import org.mandarin.booking.domain.show.ShowDetailResponse.HallResponse; import org.mandarin.booking.domain.show.ShowException; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.show.ShowRegisterResponse; @@ -27,6 +29,7 @@ class ShowService implements ShowRegisterer, ShowFetcher { private final ShowCommandRepository commandRepository; private final ShowQueryRepository queryRepository; private final HallValidator hallValidator; + private final HallFetcher hallFetcher; @Override public ShowRegisterResponse register(ShowRegisterRequest request) { @@ -65,7 +68,19 @@ public SliceView fetchShows(Integer page, Integer size, String typ @Override public ShowDetailResponse fetchShowDetail(Long showId) { var show = queryRepository.findById(showId); - return null; + var hall = hallFetcher.fetch(show.getHallId()); + return new ShowDetailResponse( + null, + null, + null, + null, + null, + null, + null, + null, + new HallResponse(hall.getId(), hall.getName()), + null + ); } private void checkDuplicateTitle(String title) { diff --git a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java index b6e3d93..359b0e5 100644 --- a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java +++ b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java @@ -179,6 +179,12 @@ public Member findMemberByUserId(String userId) { .getSingleResult(); } + public Hall findHallById(Long hallId) { + return entityManager.createQuery("SELECT h FROM Hall h WHERE h.id = :hallId", Hall.class) + .setParameter("hallId", hallId) + .getSingleResult(); + } + private void generateShow(Long hallId, Type type) { var request = validShowRegisterRequest(hallId, type.name(), randomEnum(Rating.class).name()); var show = Show.create(hallId, ShowCreateCommand.from(request)); diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 7626f50..dae8e7c 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mandarin.booking.domain.show.ShowDetailResponse; import org.mandarin.booking.domain.show.ShowResponse; import org.mandarin.booking.utils.IntegrationTest; import org.mandarin.booking.utils.IntegrationTestUtils; @@ -68,4 +69,27 @@ class GET_specs { // Assert assertThat(response.getStatus()).isEqualTo(BAD_REQUEST); } + + @Test + void 존재하는_공연장_ID가_조회된다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + + // Act + var response = testUtils.get("/api/show/" + show.getId()) + .assertSuccess(ShowDetailResponse.class); + + // Assert + var hall = response.getData().hall(); + var hallId = hall.hallId(); + var hallName = hall.hallName(); + var fetched = testFixture.findHallById(hallId); + + assertThat(fetched).isNotNull(); + assertThat(fetched.getName()).isEqualTo(hallName); + assertThat(fetched.getId()).isEqualTo(hallId); + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 62a7bcc..355042b 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -76,7 +76,7 @@ GET /api/show/1 - [x] 존재하지 않는 showId 요청 시 NOT_FOUND를 반환한다 - [x] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 - [x] 공연에 회차가 없는 경우 schedules는 빈 배열이다 -- [ ] 존재하는 공연장 ID가 조회된다 +- [x] 존재하는 공연장 ID가 조회된다 - [ ] 공연 일정은 마감 이전의 일정만 조회된다 - [ ] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 - [ ] hall 정보는 hallId와 hallName을 모두 포함한다 From 1238946197ed795699952bdeaa0db20190b309ff Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 16:06:05 +0900 Subject: [PATCH 07/16] add retrieval of schedules for shows before the performance end date --- .../booking/app/show/ShowService.java | 6 +++--- .../booking/webapi/show/showId/GET_specs.java | 20 +++++++++++++++++++ docs/specs/api/show_detail.md | 2 +- .../mandarin/booking/domain/show/Show.java | 13 ++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java index c0d5ead..3e647ab 100644 --- a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java @@ -76,10 +76,10 @@ public ShowDetailResponse fetchShowDetail(Long showId) { null, null, null, - null, - null, + show.getPerformanceStartDate(), + show.getPerformanceEndDate(), new HallResponse(hall.getId(), hall.getName()), - null + show.getScheduleResponses() ); } diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index dae8e7c..4142171 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -92,4 +92,24 @@ class GET_specs { assertThat(fetched.getName()).isEqualTo(hallName); assertThat(fetched.getId()).isEqualTo(hallId); } + + @Test + void 공연_일정은_마감_이전의_일정만_조회된다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + + // Act + var response = testUtils.get("/api/show/" + show.getId()) + .assertSuccess(ShowDetailResponse.class); + + // Assert + var schedules = response.getData().schedules(); + assertThat(schedules) + .allMatch(schedule -> schedule.getEndAt() + .isBefore(response.getData().performanceEndDate().atStartOfDay())); + + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 355042b..8b55c8a 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -77,7 +77,7 @@ GET /api/show/1 - [x] 양의 정수가 아닌 showId 요청 시 BAD_REQUEST을 반환한다 - [x] 공연에 회차가 없는 경우 schedules는 빈 배열이다 - [x] 존재하는 공연장 ID가 조회된다 -- [ ] 공연 일정은 마감 이전의 일정만 조회된다 +- [x] 공연 일정은 마감 이전의 일정만 조회된다 - [ ] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 - [ ] hall 정보는 hallId와 hallName을 모두 포함한다 - [ ] schedules는 startAt ASC, 이름 순으로 정렬되어 반환된다 diff --git a/domain/src/main/java/org/mandarin/booking/domain/show/Show.java b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java index e35291a..e058342 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/show/Show.java +++ b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java @@ -16,6 +16,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.mandarin.booking.domain.AbstractEntity; +import org.mandarin.booking.domain.show.ShowDetailResponse.ShowScheduleResponse; @Entity @Table(name = "shows") @@ -65,6 +66,18 @@ public void registerSchedule(ShowScheduleCreateCommand command) { this.schedules.add(schedule); } + public List getScheduleResponses() { + return this.schedules.stream() + .map( + schedule -> new ShowScheduleResponse( + schedule.getId(), + schedule.getStartAt(), + schedule.getEndAt() + ) + ) + .toList(); + } + public static Show create(Long hallId, ShowCreateCommand command) { var startDate = command.getPerformanceStartDate(); var endDate = command.getPerformanceEndDate(); From 13ba93cac65dbebe3fdab9b13b14ecb93fc5b27f Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 16:10:15 +0900 Subject: [PATCH 08/16] add runtime validation for show schedules to match start and end times --- .../booking/webapi/show/showId/GET_specs.java | 21 +++++++++++++++++++ docs/specs/api/show_detail.md | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 4142171..dd599b0 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -5,6 +5,7 @@ import static org.mandarin.booking.adapter.ApiStatus.NOT_FOUND; import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; +import java.time.Duration; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -112,4 +113,24 @@ class GET_specs { .isBefore(response.getData().performanceEndDate().atStartOfDay())); } + + @Test + void 공연_일정의_런타임은_시작_시간과_종료_시간의_차이와_일치한다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + + // Act + var response = testUtils.get("/api/show/" + show.getId()) + .assertSuccess(ShowDetailResponse.class); + + // Assert + var schedules = response.getData().schedules(); + + assertThat(schedules) + .allMatch(schedule -> schedule.getRuntimeMinutes() == Duration.between(schedule.getStartAt(), + schedule.getEndAt()).toMinutes()); + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 8b55c8a..d14b835 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -78,6 +78,6 @@ GET /api/show/1 - [x] 공연에 회차가 없는 경우 schedules는 빈 배열이다 - [x] 존재하는 공연장 ID가 조회된다 - [x] 공연 일정은 마감 이전의 일정만 조회된다 -- [ ] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 +- [x] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 - [ ] hall 정보는 hallId와 hallName을 모두 포함한다 - [ ] schedules는 startAt ASC, 이름 순으로 정렬되어 반환된다 From 94250fb38dbf9aa51031ebe2e44af3c69040625e Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 16:15:03 +0900 Subject: [PATCH 09/16] add test to ensure hall information includes both hallId and hallName --- .../booking/webapi/show/showId/GET_specs.java | 18 ++++++++++++++++++ docs/specs/api/show_detail.md | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index dd599b0..8b957ac 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -133,4 +133,22 @@ class GET_specs { .allMatch(schedule -> schedule.getRuntimeMinutes() == Duration.between(schedule.getStartAt(), schedule.getEndAt()).toMinutes()); } + + @Test + void hall_정보는_hallId와_hallName을_모두_포함한다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + + // Act + var response = testUtils.get("/api/show/" + show.getId()) + .assertSuccess(ShowDetailResponse.class); + + // Assert + var hall = response.getData().hall(); + assertThat(hall.hallId()).isNotNull(); + assertThat(hall.hallName()).isNotNull(); + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index d14b835..b1672e1 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -79,5 +79,5 @@ GET /api/show/1 - [x] 존재하는 공연장 ID가 조회된다 - [x] 공연 일정은 마감 이전의 일정만 조회된다 - [x] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 -- [ ] hall 정보는 hallId와 hallName을 모두 포함한다 +- [x] hall 정보는 hallId와 hallName을 모두 포함한다 - [ ] schedules는 startAt ASC, 이름 순으로 정렬되어 반환된다 From 774be5355c15692c5e45bac0d56f017aaa0a489e Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 16:15:30 +0900 Subject: [PATCH 10/16] remove hall information test for show detail retrieval --- .../booking/webapi/show/showId/GET_specs.java | 18 ------------------ docs/specs/api/show_detail.md | 1 - 2 files changed, 19 deletions(-) diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 8b957ac..dd599b0 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -133,22 +133,4 @@ class GET_specs { .allMatch(schedule -> schedule.getRuntimeMinutes() == Duration.between(schedule.getStartAt(), schedule.getEndAt()).toMinutes()); } - - @Test - void hall_정보는_hallId와_hallName을_모두_포함한다( - @Autowired IntegrationTestUtils testUtils, - @Autowired TestFixture testFixture - ) { - // Arrange - var show = testFixture.generateShow(5); - - // Act - var response = testUtils.get("/api/show/" + show.getId()) - .assertSuccess(ShowDetailResponse.class); - - // Assert - var hall = response.getData().hall(); - assertThat(hall.hallId()).isNotNull(); - assertThat(hall.hallName()).isNotNull(); - } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index b1672e1..15aa779 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -79,5 +79,4 @@ GET /api/show/1 - [x] 존재하는 공연장 ID가 조회된다 - [x] 공연 일정은 마감 이전의 일정만 조회된다 - [x] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 -- [x] hall 정보는 hallId와 hallName을 모두 포함한다 - [ ] schedules는 startAt ASC, 이름 순으로 정렬되어 반환된다 From ab4e893ab7449197b8ceaafba7f076c55a35464a Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 16:50:04 +0900 Subject: [PATCH 11/16] add test to verify schedules are sorted by end time in show detail retrieval --- .../mandarin/booking/utils/TestFixture.java | 6 +++--- .../booking/webapi/show/showId/GET_specs.java | 20 +++++++++++++++++++ docs/specs/api/show_detail.md | 2 +- .../mandarin/booking/domain/show/Show.java | 6 ++++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java index 359b0e5..d5979db 100644 --- a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java +++ b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java @@ -95,7 +95,7 @@ public Show generateShow(int scheduleCount) { var hall = insertDummyHall(); var show = generateShow(hall.getId()); - IntStream.range(0, scheduleCount).forEach(i -> { + for (int i = 0; i < scheduleCount; i++) { Random random = new Random(); var startAt = LocalDateTime.now().plusDays(random.nextInt(0, 10)); var command = new ShowScheduleCreateCommand(show.getId(), @@ -103,7 +103,7 @@ public Show generateShow(int scheduleCount) { startAt.plusHours(random.nextInt(2, 5)) ); show.registerSchedule(command); - }); + } return showInsert(show); } @@ -210,7 +210,7 @@ private void generateShow(Long hallId, Rating rating) { showInsert(show); } - private Show generateShow(Long hallId) { + public Show generateShow(Long hallId) { var request = validShowRegisterRequest(hallId, randomEnum(Type.class).name(), randomEnum(Rating.class).name()); var show = Show.create(hallId, ShowCreateCommand.from(request)); return showInsert(show); diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index dd599b0..690294a 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -6,11 +6,13 @@ import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; import java.time.Duration; +import java.util.Comparator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mandarin.booking.domain.show.ShowDetailResponse; +import org.mandarin.booking.domain.show.ShowDetailResponse.ShowScheduleResponse; import org.mandarin.booking.domain.show.ShowResponse; import org.mandarin.booking.utils.IntegrationTest; import org.mandarin.booking.utils.IntegrationTestUtils; @@ -133,4 +135,22 @@ class GET_specs { .allMatch(schedule -> schedule.getRuntimeMinutes() == Duration.between(schedule.getStartAt(), schedule.getEndAt()).toMinutes()); } + + @Test + void schedules는_endAt_ASC_순으로_정렬되어_반환된다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + + // Act + var response = testUtils.get("/api/show/" + show.getId()) + .assertSuccess(ShowDetailResponse.class); + + // Assert + var schedules = response.getData().schedules(); + assertThat(schedules).isNotEmpty(); + assertThat(schedules).isSortedAccordingTo(Comparator.comparing(ShowScheduleResponse::getEndAt)); + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 15aa779..0a33daf 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -79,4 +79,4 @@ GET /api/show/1 - [x] 존재하는 공연장 ID가 조회된다 - [x] 공연 일정은 마감 이전의 일정만 조회된다 - [x] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 -- [ ] schedules는 startAt ASC, 이름 순으로 정렬되어 반환된다 +- [ ] schedules는 endAt ASC 순으로 정렬되어 반환된다 diff --git a/domain/src/main/java/org/mandarin/booking/domain/show/Show.java b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java index e058342..ff35c7a 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/show/Show.java +++ b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java @@ -1,6 +1,6 @@ package org.mandarin.booking.domain.show; -import static jakarta.persistence.CascadeType.MERGE; +import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.FetchType.LAZY; import jakarta.persistence.Entity; @@ -11,6 +11,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import lombok.AccessLevel; import lombok.Getter; @@ -23,7 +24,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Show extends AbstractEntity { - @OneToMany(mappedBy = "show", fetch = LAZY, cascade = MERGE) + @OneToMany(mappedBy = "show", fetch = LAZY, cascade = ALL) private final List schedules = new ArrayList<>(); private Long hallId; @@ -68,6 +69,7 @@ public void registerSchedule(ShowScheduleCreateCommand command) { public List getScheduleResponses() { return this.schedules.stream() + .sorted(Comparator.comparing(ShowSchedule::getEndAt)) .map( schedule -> new ShowScheduleResponse( schedule.getId(), From a66a866cc8e9761a1867b25b54e14ce6f98996ec Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 16:50:28 +0900 Subject: [PATCH 12/16] add test to verify schedules are sorted by end time in show detail retrieval --- docs/specs/api/show_detail.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 0a33daf..1fa6e68 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -79,4 +79,4 @@ GET /api/show/1 - [x] 존재하는 공연장 ID가 조회된다 - [x] 공연 일정은 마감 이전의 일정만 조회된다 - [x] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 -- [ ] schedules는 endAt ASC 순으로 정렬되어 반환된다 +- [x] schedules는 endAt ASC 순으로 정렬되어 반환된다 From 8e5fc2d601409ca06f5eff0feb8c365eee9ab903 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 17:03:01 +0900 Subject: [PATCH 13/16] add test to verify retrieval of persisted show details --- .../booking/app/show/ShowService.java | 12 +++---- .../mandarin/booking/utils/TestFixture.java | 9 ++++++ .../booking/webapi/show/showId/GET_specs.java | 32 +++++++++++++++++++ docs/specs/api/show_detail.md | 2 ++ .../domain/show/ShowDetailResponse.java | 6 ++-- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java index 3e647ab..5d071a9 100644 --- a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java @@ -70,12 +70,12 @@ public ShowDetailResponse fetchShowDetail(Long showId) { var show = queryRepository.findById(showId); var hall = hallFetcher.fetch(show.getHallId()); return new ShowDetailResponse( - null, - null, - null, - null, - null, - null, + show.getId(), + show.getTitle(), + show.getType(), + show.getRating(), + show.getSynopsis(), + show.getPosterUrl(), show.getPerformanceStartDate(), show.getPerformanceEndDate(), new HallResponse(hall.getId(), hall.getName()), diff --git a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java index d5979db..46ea6f1 100644 --- a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java +++ b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java @@ -22,6 +22,7 @@ import org.mandarin.booking.domain.show.Show.Rating; import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.Show.Type; +import org.mandarin.booking.domain.show.ShowDetailResponse.ShowScheduleResponse; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.show.ShowScheduleCreateCommand; import org.springframework.test.util.ReflectionTestUtils; @@ -185,6 +186,14 @@ public Hall findHallById(Long hallId) { .getSingleResult(); } + public boolean isMatchingScheduleInShow(ShowScheduleResponse res, Show show) { + return !entityManager.createQuery( + "SELECT s FROM ShowSchedule s WHERE s.id = :scheduleId AND s.show.id = :showId", Object.class) + .setParameter("scheduleId", res.getScheduleId()) + .setParameter("showId", show.getId()) + .getResultList().isEmpty(); + } + private void generateShow(Long hallId, Type type) { var request = validShowRegisterRequest(hallId, type.name(), randomEnum(Rating.class).name()); var show = Show.create(hallId, ShowCreateCommand.from(request)); diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 690294a..4598c2f 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -1,6 +1,7 @@ package org.mandarin.booking.webapi.show.showId; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatStream; import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.ApiStatus.NOT_FOUND; import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; @@ -153,4 +154,35 @@ class GET_specs { assertThat(schedules).isNotEmpty(); assertThat(schedules).isSortedAccordingTo(Comparator.comparing(ShowScheduleResponse::getEndAt)); } + + @Test + void 영속화된_정보가_조회된다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShow(5); + + // Act + var response = testUtils.get("/api/show/" + show.getId()) + .assertSuccess(ShowDetailResponse.class); + + // Assert + var data = response.getData(); + assertThat(data.showId()).isEqualTo(show.getId()); + assertThat(data.title()).isEqualTo(show.getTitle()); + assertThat(data.type()).isEqualTo(show.getType()); + assertThat(data.rating()).isEqualTo(show.getRating()); + assertThat(data.synopsis()).isEqualTo(show.getSynopsis()); + assertThat(data.posterUrl()).isEqualTo(show.getPosterUrl()); + assertThat(data.performanceStartDate()).isEqualTo(show.getPerformanceStartDate()); + assertThat(data.performanceEndDate()).isEqualTo(show.getPerformanceEndDate()); + + var schedules = data.schedules(); + assertThat(schedules).hasSize(show.getSchedules().size()); + assertThatStream(schedules.stream()) + .allMatch(res -> + testFixture.isMatchingScheduleInShow(res, show) + ); + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index 1fa6e68..ea3bf9e 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -80,3 +80,5 @@ GET /api/show/1 - [x] 공연 일정은 마감 이전의 일정만 조회된다 - [x] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 - [x] schedules는 endAt ASC 순으로 정렬되어 반환된다 +- [x] 영속화된 정보가 조회된다 +- [ ] synopsis가 null인 경우 빈 문자열로 반환된다 diff --git a/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java index 8edc0fb..077e237 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java +++ b/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java @@ -5,12 +5,14 @@ import java.time.LocalDateTime; import java.util.List; import lombok.Getter; +import org.mandarin.booking.domain.show.Show.Rating; +import org.mandarin.booking.domain.show.Show.Type; public record ShowDetailResponse( Long showId, String title, - String type, - String rating, + Type type, + Rating rating, String synopsis, String posterUrl, LocalDate performanceStartDate, From 9e95f8e03b733f375aa610464a701bbe8c1f51a1 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 26 Sep 2025 17:07:08 +0900 Subject: [PATCH 14/16] add test to verify retrieval of persisted show details --- .../mandarin/booking/utils/TestFixture.java | 27 +++++++++++++++++++ .../booking/webapi/show/showId/GET_specs.java | 19 +++++++++++++ docs/specs/api/show_detail.md | 2 +- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java index 46ea6f1..23620d2 100644 --- a/application/src/test/java/org/mandarin/booking/utils/TestFixture.java +++ b/application/src/test/java/org/mandarin/booking/utils/TestFixture.java @@ -164,6 +164,33 @@ public void generateShows(int showCount, int before, int after) { }); } + public Show generateShowWithNoSynopsis(int scheduleCount) { + var hall = insertDummyHall(); + var show = Show.create(hall.getId(), ShowCreateCommand.from(new ShowRegisterRequest( + hall.getId(), + UUID.randomUUID().toString().substring(0, 10), + randomEnum(Type.class).name(), + randomEnum(Rating.class).name(), + null, + "https://example.com/poster.jpg", + LocalDate.now(), + LocalDate.now().plusDays(30) + ))); + + for (int i = 0; i < scheduleCount; i++) { + Random random = new Random(); + var startAt = LocalDateTime.now().plusDays(random.nextInt(0, 10)); + var command = new ShowScheduleCreateCommand(show.getId(), + startAt, + startAt.plusHours(random.nextInt(2, 5)) + ); + show.registerSchedule(command); + } + + ReflectionTestUtils.setField(show, "synopsis", ""); + return showInsert(show); + } + public boolean existsHallName(String name) { return (entityManager.createQuery("SELECT COUNT(h) FROM Hall h WHERE h.name = :name") .setParameter("name", name) diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 4598c2f..3a6046f 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -185,4 +185,23 @@ class GET_specs { testFixture.isMatchingScheduleInShow(res, show) ); } + + @Test + void synopsis가_없는_경우_빈_문자열로_반환된다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TestFixture testFixture + ) { + // Arrange + var show = testFixture.generateShowWithNoSynopsis(5); + + // Act + var response = testUtils.get("/api/show/" + show.getId()) + .assertSuccess(ShowDetailResponse.class); + + // Assert + var data = response.getData(); + + assertThat(data.synopsis()).isNotNull(); + assertThat(data.synopsis()).isEmpty(); + } } diff --git a/docs/specs/api/show_detail.md b/docs/specs/api/show_detail.md index ea3bf9e..b548edc 100644 --- a/docs/specs/api/show_detail.md +++ b/docs/specs/api/show_detail.md @@ -81,4 +81,4 @@ GET /api/show/1 - [x] 공연 일정의 런타임은 시작 시간과 종료 시간의 차이와 일치한다 - [x] schedules는 endAt ASC 순으로 정렬되어 반환된다 - [x] 영속화된 정보가 조회된다 -- [ ] synopsis가 null인 경우 빈 문자열로 반환된다 +- [x] synopsis가 없는 경우 빈 문자열로 반환된다 From 95423c96962bd0d6069156f66f473fa2dab4dfc8 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Sat, 27 Sep 2025 15:10:51 +0900 Subject: [PATCH 15/16] simplify ShowDetailResponse structure and update related methods --- .../booking/app/show/ShowService.java | 4 +-- .../booking/webapi/show/showId/GET_specs.java | 5 ++-- .../mandarin/booking/domain/show/Show.java | 2 +- .../domain/show/ShowDetailResponse.java | 25 ++++++------------- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java index 5d071a9..6b0aebc 100644 --- a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java @@ -13,7 +13,6 @@ import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.Show.Type; import org.mandarin.booking.domain.show.ShowDetailResponse; -import org.mandarin.booking.domain.show.ShowDetailResponse.HallResponse; import org.mandarin.booking.domain.show.ShowException; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.show.ShowRegisterResponse; @@ -78,7 +77,8 @@ public ShowDetailResponse fetchShowDetail(Long showId) { show.getPosterUrl(), show.getPerformanceStartDate(), show.getPerformanceEndDate(), - new HallResponse(hall.getId(), hall.getName()), + hall.getId(), + hall.getName(), show.getScheduleResponses() ); } diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java index 3a6046f..216954b 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/showId/GET_specs.java @@ -87,9 +87,8 @@ class GET_specs { .assertSuccess(ShowDetailResponse.class); // Assert - var hall = response.getData().hall(); - var hallId = hall.hallId(); - var hallName = hall.hallName(); + var hallId = response.getData().hallId(); + var hallName = response.getData().hallName(); var fetched = testFixture.findHallById(hallId); assertThat(fetched).isNotNull(); diff --git a/domain/src/main/java/org/mandarin/booking/domain/show/Show.java b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java index ff35c7a..af5d098 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/show/Show.java +++ b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java @@ -67,7 +67,7 @@ public void registerSchedule(ShowScheduleCreateCommand command) { this.schedules.add(schedule); } - public List getScheduleResponses() { + public List getScheduleResponses() { return this.schedules.stream() .sorted(Comparator.comparing(ShowSchedule::getEndAt)) .map( diff --git a/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java index 077e237..939a698 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java +++ b/domain/src/main/java/org/mandarin/booking/domain/show/ShowDetailResponse.java @@ -1,5 +1,6 @@ package org.mandarin.booking.domain.show; +import com.querydsl.core.annotations.QueryProjection; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; @@ -8,31 +9,21 @@ import org.mandarin.booking.domain.show.Show.Rating; import org.mandarin.booking.domain.show.Show.Type; -public record ShowDetailResponse( - Long showId, - String title, - Type type, - Rating rating, - String synopsis, - String posterUrl, - LocalDate performanceStartDate, - LocalDate performanceEndDate, - HallResponse hall, - List schedules -) { - public record HallResponse( - Long hallId, - String hallName - ) { +public record ShowDetailResponse(Long showId, String title, Type type, Rating rating, String synopsis, String posterUrl, + LocalDate performanceStartDate, LocalDate performanceEndDate, Long hallId, + String hallName, List schedules) { + @QueryProjection + public ShowDetailResponse { } @Getter - public static final class ShowScheduleResponse { + public static class ShowScheduleResponse { private final Long scheduleId; private final LocalDateTime startAt; private final LocalDateTime endAt; private final long runtimeMinutes; + @QueryProjection public ShowScheduleResponse(Long scheduleId, LocalDateTime startAt, LocalDateTime endAt) { this.scheduleId = scheduleId; this.startAt = startAt; From 58b6e87887e28e4750a616b6a3d0d339ef4f46dc Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Sat, 27 Sep 2025 15:22:24 +0900 Subject: [PATCH 16/16] complete show detail retrieval functionality and note performance considerations --- docs/devlog/250927.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/devlog/250927.md diff --git a/docs/devlog/250927.md b/docs/devlog/250927.md new file mode 100644 index 0000000..6dcf83f --- /dev/null +++ b/docs/devlog/250927.md @@ -0,0 +1,6 @@ +## 예찬 + +공연 상세 목록 조회 기능을 완료했다. 기능 자체가 복잡하지는 않은데 리팩터링 과정에서 알게된 사실이 있다. AR을 기준으로 연관을 전부 쪼개놔서 그런가 생각보다 공연 상세를 조회하는 과정이 빠른 성능을 보이지는 +않는다. + +QueryDSL로 성능을 빠르게 뽑아보려고 시도를 해보기도 했지만 그리 빠르지가 않다. 역시 찍어보지 않고서는 모른는건가? 물론 인덱스 처리를 하지 않은 상황이라 튜닝후 결과를 보기 전에는 모르