diff --git a/backend/src/main/java/com/festago/admin/application/AdminStageV1QueryService.java b/backend/src/main/java/com/festago/admin/application/AdminStageV1QueryService.java index f871c40dc..298535195 100644 --- a/backend/src/main/java/com/festago/admin/application/AdminStageV1QueryService.java +++ b/backend/src/main/java/com/festago/admin/application/AdminStageV1QueryService.java @@ -2,6 +2,8 @@ import com.festago.admin.dto.stage.AdminStageV1Response; import com.festago.admin.repository.AdminStageV1QueryDslRepository; +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.NotFoundException; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,4 +19,9 @@ public class AdminStageV1QueryService { public List findAllByFestivalId(Long festivalId) { return adminStageV1QueryDslRepository.findAllByFestivalId(festivalId); } + + public AdminStageV1Response findById(Long stageId) { + return adminStageV1QueryDslRepository.findById(stageId) + .orElseThrow(() -> new NotFoundException(ErrorCode.STAGE_NOT_FOUND)); + } } diff --git a/backend/src/main/java/com/festago/admin/presentation/v1/AdminStageV1Controller.java b/backend/src/main/java/com/festago/admin/presentation/v1/AdminStageV1Controller.java index 4ac30fefa..746d1f3f6 100644 --- a/backend/src/main/java/com/festago/admin/presentation/v1/AdminStageV1Controller.java +++ b/backend/src/main/java/com/festago/admin/presentation/v1/AdminStageV1Controller.java @@ -1,5 +1,7 @@ package com.festago.admin.presentation.v1; +import com.festago.admin.application.AdminStageV1QueryService; +import com.festago.admin.dto.stage.AdminStageV1Response; import com.festago.admin.dto.stage.StageV1CreateRequest; import com.festago.admin.dto.stage.StageV1UpdateRequest; import com.festago.stage.application.command.StageCommandFacadeService; @@ -9,6 +11,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -23,6 +26,15 @@ public class AdminStageV1Controller { private final StageCommandFacadeService stageCommandFacadeService; + private final AdminStageV1QueryService adminStageV1QueryService; + + @GetMapping("/{stageId}") + public ResponseEntity findById( + @PathVariable Long stageId + ) { + return ResponseEntity.ok() + .body(adminStageV1QueryService.findById(stageId)); + } @PostMapping public ResponseEntity createStage( diff --git a/backend/src/main/java/com/festago/admin/repository/AdminStageV1QueryDslRepository.java b/backend/src/main/java/com/festago/admin/repository/AdminStageV1QueryDslRepository.java index b11b98164..853178894 100644 --- a/backend/src/main/java/com/festago/admin/repository/AdminStageV1QueryDslRepository.java +++ b/backend/src/main/java/com/festago/admin/repository/AdminStageV1QueryDslRepository.java @@ -12,6 +12,7 @@ import com.festago.common.querydsl.QueryDslRepositorySupport; import com.festago.stage.domain.Stage; import java.util.List; +import java.util.Optional; import org.springframework.stereotype.Repository; @Repository @@ -43,4 +44,30 @@ public List findAllByFestivalId(Long festivalId) { ) ); } + + public Optional findById(Long stageId) { + List response = selectFrom(stage) + .leftJoin(stageArtist).on(stageArtist.stageId.eq(stageId)) + .leftJoin(artist).on(artist.id.eq(stageArtist.artistId)) + .where(stage.id.eq(stageId)) + .transform( + groupBy(stage.id).list( + new QAdminStageV1Response( + stage.id, + stage.startTime, + stage.ticketOpenTime, + list(new QAdminStageArtistV1Response( + artist.id, + artist.name + )), + stage.createdAt, + stage.updatedAt + ) + ) + ); + if (response.isEmpty()) { + return Optional.empty(); + } + return Optional.of(response.get(0)); + } } diff --git a/backend/src/test/java/com/festago/admin/application/integration/AdminStageV1QueryServiceIntegrationTest.java b/backend/src/test/java/com/festago/admin/application/integration/AdminStageV1QueryServiceIntegrationTest.java index 634760ba4..5fbc43a08 100644 --- a/backend/src/test/java/com/festago/admin/application/integration/AdminStageV1QueryServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/admin/application/integration/AdminStageV1QueryServiceIntegrationTest.java @@ -2,12 +2,16 @@ import static java.util.stream.Collectors.toMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.festago.admin.application.AdminStageV1QueryService; import com.festago.admin.dto.stage.AdminStageArtistV1Response; import com.festago.admin.dto.stage.AdminStageV1Response; +import com.festago.artist.domain.Artist; import com.festago.artist.repository.ArtistRepository; +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.NotFoundException; import com.festago.festival.domain.Festival; import com.festago.festival.repository.FestivalRepository; import com.festago.school.repository.SchoolRepository; @@ -55,116 +59,187 @@ class AdminStageV1QueryServiceIntegrationTest extends ApplicationIntegrationTest LocalDate _2077년_6월_16일 = LocalDate.parse("2077-06-16"); LocalDate _2077년_6월_17일 = LocalDate.parse("2077-07-17"); - @Test - void 존재하지_않는_축제의_식별자로_조회하면_빈_리스트가_반환된다() { - // when - var actual = adminStageV1QueryService.findAllByFestivalId(4885L); - - // then - assertThat(actual).isEmpty(); - } - @Nested - class 축제에_공연이_없으면 { - - Long 축제_식별자; - - @BeforeEach - void setUp() { - var 학교 = schoolRepository.save(SchoolFixture.builder().build()); - 축제_식별자 = festivalRepository.save(FestivalFixture.builder() - .startDate(_2077년_6월_15일) - .endDate(_2077년_6월_15일) - .school(학교) - .build()).getId(); - } + class findAllByFestivalId { @Test - void 빈_리스트가_반환된다() { + void 존재하지_않는_축제의_식별자로_조회하면_빈_리스트가_반환된다() { // when - var actual = adminStageV1QueryService.findAllByFestivalId(축제_식별자); + var actual = adminStageV1QueryService.findAllByFestivalId(4885L); // then assertThat(actual).isEmpty(); } - } - /** - * 6월 15일 ~ 6월 17일까지 진행되는 축제

6월 15일 공연, 6월 16일 공연이 있다.

- *

- * 6월 15일 공연에는 아티스트A, 아티스트B가 참여한다.

6월 16일 공연에는 아티스트C가 참여한다. - */ - @Nested - class 특정_축제의_공연_목록과_참여하는_아티스트를_조회할_수_있어야_한다 { - - Long 아티스트A_식별자; - Long 아티스트B_식별자; - Long 아티스트C_식별자; - Festival 축제; - Long _6월_15일_공연_식별자; - Long _6월_16일_공연_식별자; - - @BeforeEach - void setUp() { - 아티스트A_식별자 = createArtist("아티스트A"); - 아티스트B_식별자 = createArtist("아티스트B"); - 아티스트C_식별자 = createArtist("아티스트C"); - var 학교 = schoolRepository.save(SchoolFixture.builder().build()); - 축제 = festivalRepository.save(FestivalFixture.builder() - .startDate(_2077년_6월_15일) - .endDate(_2077년_6월_17일) - .school(학교) - .build()); - _6월_15일_공연_식별자 = createStage(축제, _2077년_6월_15일, List.of(아티스트A_식별자, 아티스트B_식별자)).getId(); - _6월_16일_공연_식별자 = createStage(축제, _2077년_6월_16일, List.of(아티스트C_식별자)).getId(); - } + @Nested + class 축제에_공연이_없으면 { + + Long 축제_식별자; + + @BeforeEach + void setUp() { + var 학교 = schoolRepository.save(SchoolFixture.builder().build()); + 축제_식별자 = festivalRepository.save(FestivalFixture.builder() + .startDate(_2077년_6월_15일) + .endDate(_2077년_6월_15일) + .school(학교) + .build()).getId(); + } - private Long createArtist(String artistName) { - return artistRepository.save(ArtistFixture.builder() - .name(artistName) - .build() - ).getId(); + @Test + void 빈_리스트가_반환된다() { + // when + var actual = adminStageV1QueryService.findAllByFestivalId(축제_식별자); + + // then + assertThat(actual).isEmpty(); + } } - private Stage createStage(Festival festival, LocalDate localDate, List artistIds) { - var 공연 = stageRepository.save(StageFixture.builder() - .festival(festival) - .startTime(localDate.atTime(18, 0)) - .build() - ); - for (Long artistId : artistIds) { - stageArtistRepository.save(StageArtistFixture.builder(공연.getId(), artistId).build()); + /** + * 6월 15일 ~ 6월 17일까지 진행되는 축제

6월 15일 공연, 6월 16일 공연이 있다.

+ *

+ * 6월 15일 공연에는 아티스트A, 아티스트B가 참여한다.

6월 16일 공연에는 아티스트C가 참여한다. + */ + @Nested + class 축제에_공연이_있으면 { + + Long 아티스트A_식별자; + Long 아티스트B_식별자; + Long 아티스트C_식별자; + Festival 축제; + Long _6월_15일_공연_식별자; + Long _6월_16일_공연_식별자; + + @BeforeEach + void setUp() { + 아티스트A_식별자 = createArtist("아티스트A").getId(); + 아티스트B_식별자 = createArtist("아티스트B").getId(); + 아티스트C_식별자 = createArtist("아티스트C").getId(); + var 학교 = schoolRepository.save(SchoolFixture.builder().build()); + 축제 = festivalRepository.save(FestivalFixture.builder() + .startDate(_2077년_6월_15일) + .endDate(_2077년_6월_17일) + .school(학교) + .build()); + _6월_15일_공연_식별자 = createStage(축제, _2077년_6월_15일, List.of(아티스트A_식별자, 아티스트B_식별자)).getId(); + _6월_16일_공연_식별자 = createStage(축제, _2077년_6월_16일, List.of(아티스트C_식별자)).getId(); + } + + @Test + void 공연의_시작_순서대로_정렬된다() { + // when + var actual = adminStageV1QueryService.findAllByFestivalId(축제.getId()); + + // then + assertThat(actual) + .map(AdminStageV1Response::id) + .containsExactly(_6월_15일_공연_식별자, _6월_16일_공연_식별자); + } + + @Test + void 해당_일자의_공연에_참여하는_아티스트_목록을_조회할_수_있다() { + // when + var stageIdToArtists = adminStageV1QueryService.findAllByFestivalId(축제.getId()).stream() + .collect(toMap(AdminStageV1Response::id, AdminStageV1Response::artists)); + + // then + assertSoftly(softly -> { + softly.assertThat(stageIdToArtists.get(_6월_15일_공연_식별자)) + .map(AdminStageArtistV1Response::id) + .containsExactlyInAnyOrder(아티스트A_식별자, 아티스트B_식별자); + + softly.assertThat(stageIdToArtists.get(_6월_16일_공연_식별자)) + .map(AdminStageArtistV1Response::id) + .containsExactlyInAnyOrder(아티스트C_식별자); + }); } - return 공연; } + } - @Test - void 공연의_시작_순서대로_정렬된다() { - // when - var actual = adminStageV1QueryService.findAllByFestivalId(축제.getId()); + private Artist createArtist(String artistName) { + return artistRepository.save(ArtistFixture.builder() + .name(artistName) + .build() + ); + } - // then - assertThat(actual) - .map(AdminStageV1Response::id) - .containsExactly(_6월_15일_공연_식별자, _6월_16일_공연_식별자); + private Stage createStage(Festival festival, LocalDate localDate, List artistIds) { + var 공연 = stageRepository.save(StageFixture.builder() + .festival(festival) + .startTime(localDate.atTime(18, 0)) + .build() + ); + for (Long artistId : artistIds) { + stageArtistRepository.save(StageArtistFixture.builder(공연.getId(), artistId).build()); } + return 공연; + } - @Test - void 해당_일자의_공연에_참여하는_아티스트_목록을_조회할_수_있다() { - // when - var stageIdToArtists = adminStageV1QueryService.findAllByFestivalId(축제.getId()).stream() - .collect(toMap(AdminStageV1Response::id, AdminStageV1Response::artists)); + @Nested + class findById { - // then - assertSoftly(softly -> { - softly.assertThat(stageIdToArtists.get(_6월_15일_공연_식별자)) - .map(AdminStageArtistV1Response::id) - .containsExactlyInAnyOrder(아티스트A_식별자, 아티스트B_식별자); - - softly.assertThat(stageIdToArtists.get(_6월_16일_공연_식별자)) - .map(AdminStageArtistV1Response::id) - .containsExactlyInAnyOrder(아티스트C_식별자); - }); + @Nested + class 식별자에_해당하는_공연이_없으면 { + + @Test + void 예외가_발생한다() { + // when & then + assertThatThrownBy(() -> adminStageV1QueryService.findById(4885L)) + .isInstanceOf(NotFoundException.class) + .hasMessage(ErrorCode.STAGE_NOT_FOUND.getMessage()); + } + } + + @Nested + class 식별자에_해당하는_공연이_있으면 { + + Artist 아티스트A; + Artist 아티스트B; + Artist 아티스트C; + Stage 공연; + + @BeforeEach + void setUp() { + var 학교 = schoolRepository.save(SchoolFixture.builder().build()); + var 축제 = festivalRepository.save(FestivalFixture.builder() + .startDate(_2077년_6월_15일) + .endDate(_2077년_6월_15일) + .school(학교) + .build() + ); + 아티스트A = createArtist("아티스트A"); + 아티스트B = createArtist("아티스트B"); + 아티스트C = createArtist("아티스트C"); + 공연 = createStage( + 축제, + _2077년_6월_15일, + List.of(아티스트A.getId(), 아티스트B.getId(), 아티스트C.getId()) + ); + } + + @Test + void 공연의_정보가_정확하게_조회되어야_한다() { + // when + var actual = adminStageV1QueryService.findById(공연.getId()); + + assertSoftly(softly -> { + softly.assertThat(actual.id()).isEqualTo(공연.getId()); + softly.assertThat(actual.startDateTime()).isEqualTo(공연.getStartTime()); + softly.assertThat(actual.ticketOpenTime()).isEqualTo(공연.getTicketOpenTime()); + }); + } + + @Test + void 공연의_아티스트_목록이_조회되어야_한다() { + // when + var actual = adminStageV1QueryService.findById(공연.getId()); + + // then + assertThat(actual.artists()) + .map(AdminStageArtistV1Response::name) + .containsExactlyInAnyOrder(아티스트A.getName(), 아티스트B.getName(), 아티스트C.getName()); + } } } } diff --git a/backend/src/test/java/com/festago/admin/presentation/v1/AdminStageV1ControllerTest.java b/backend/src/test/java/com/festago/admin/presentation/v1/AdminStageV1ControllerTest.java index 9c2518499..e447d5714 100644 --- a/backend/src/test/java/com/festago/admin/presentation/v1/AdminStageV1ControllerTest.java +++ b/backend/src/test/java/com/festago/admin/presentation/v1/AdminStageV1ControllerTest.java @@ -3,6 +3,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -45,6 +46,44 @@ class AdminStageV1ControllerTest { @Autowired StageCommandFacadeService stageCommandFacadeService; + @Nested + class 공연_단건_조회 { + + final String uri = "/admin/api/v1/stages/{stageId}"; + + @Nested + @DisplayName("GET " + uri) + class 올바른_주소로 { + + Long stageId = 1L; + + @Test + @WithMockAuth(role = Role.ADMIN) + void 요청을_보내면_200_응답이_반환된다() throws Exception { + mockMvc.perform(get(uri, stageId) + .cookie(TOKEN_COOKIE) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void 토큰_없이_보내면_401_응답이_반환된다() throws Exception { + // when & then + mockMvc.perform(get(uri, 1)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockAuth(role = Role.MEMBER) + void 토큰의_권한이_Admin이_아니면_404_응답이_반환된다() throws Exception { + // when & then + mockMvc.perform(get(uri, 1) + .cookie(TOKEN_COOKIE)) + .andExpect(status().isNotFound()); + } + } + } + @Nested class 공연_생성 { @@ -58,7 +97,8 @@ class 올바른_주소로 { LocalDateTime startTime = LocalDateTime.parse("2077-06-30T18:00:00"); LocalDateTime ticketOpenTime = LocalDateTime.parse("2077-06-23T00:00:00"); List artistIds = List.of(1L, 2L, 3L); - StageV1CreateRequest request = new StageV1CreateRequest(festivalId, startTime, ticketOpenTime, artistIds); + StageV1CreateRequest request = new StageV1CreateRequest(festivalId, startTime, ticketOpenTime, + artistIds); @Test @WithMockAuth(role = Role.ADMIN) @@ -68,7 +108,7 @@ class 올바른_주소로 { .willReturn(1L); // when & then - mockMvc.perform(post(uri, 1) + mockMvc.perform(post(uri) .cookie(TOKEN_COOKIE) .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON)) @@ -79,7 +119,7 @@ class 올바른_주소로 { @Test void 토큰_없이_보내면_401_응답이_반환된다() throws Exception { // when & then - mockMvc.perform(post(uri, 1)) + mockMvc.perform(post(uri)) .andExpect(status().isUnauthorized()); } @@ -87,7 +127,7 @@ class 올바른_주소로 { @WithMockAuth(role = Role.MEMBER) void 토큰의_권한이_Admin이_아니면_404_응답이_반환된다() throws Exception { // when & then - mockMvc.perform(post(uri, 1) + mockMvc.perform(post(uri) .cookie(TOKEN_COOKIE)) .andExpect(status().isNotFound()); } @@ -112,7 +152,7 @@ class 올바른_주소로 { @WithMockAuth(role = Role.ADMIN) void 요청을_보내면_200_응답이_반환된다() throws Exception { // when & then - mockMvc.perform(patch(uri, 1, 1) + mockMvc.perform(patch(uri, 1) .cookie(TOKEN_COOKIE) .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON)) @@ -122,7 +162,7 @@ class 올바른_주소로 { @Test void 토큰_없이_보내면_401_응답이_반환된다() throws Exception { // when & then - mockMvc.perform(patch(uri, 1, 1)) + mockMvc.perform(patch(uri, 1)) .andExpect(status().isUnauthorized()); } @@ -130,7 +170,7 @@ class 올바른_주소로 { @WithMockAuth(role = Role.MEMBER) void 토큰의_권한이_Admin이_아니면_404_응답이_반환된다() throws Exception { // when & then - mockMvc.perform(patch(uri, 1, 1) + mockMvc.perform(patch(uri, 1) .cookie(TOKEN_COOKIE)) .andExpect(status().isNotFound()); } @@ -150,7 +190,7 @@ class 올바른_주소로 { @WithMockAuth(role = Role.ADMIN) void 요청을_보내면_204_응답이_반환된다() throws Exception { // when & then - mockMvc.perform(delete(uri, 1, 1) + mockMvc.perform(delete(uri, 1) .cookie(TOKEN_COOKIE)) .andExpect(status().isNoContent()); } @@ -158,7 +198,7 @@ class 올바른_주소로 { @Test void 토큰_없이_보내면_401_응답이_반환된다() throws Exception { // when & then - mockMvc.perform(delete(uri, 1, 1)) + mockMvc.perform(delete(uri, 1)) .andExpect(status().isUnauthorized()); } @@ -166,10 +206,11 @@ class 올바른_주소로 { @WithMockAuth(role = Role.MEMBER) void 토큰의_권한이_Admin이_아니면_404_응답이_반환된다() throws Exception { // when & then - mockMvc.perform(delete(uri, 1, 1) + mockMvc.perform(delete(uri, 1) .cookie(TOKEN_COOKIE)) .andExpect(status().isNotFound()); } } } + }