diff --git a/src/main/java/com/bm/getin/controller/AdminController.java b/src/main/java/com/bm/getin/controller/AdminController.java index 1c5fcea..d54946e 100644 --- a/src/main/java/com/bm/getin/controller/AdminController.java +++ b/src/main/java/com/bm/getin/controller/AdminController.java @@ -4,6 +4,7 @@ import com.bm.getin.constant.ErrorCode; import com.bm.getin.constant.EventStatus; import com.bm.getin.constant.PlaceType; +import com.bm.getin.domain.Event; import com.bm.getin.domain.Place; import com.bm.getin.dto.*; import com.bm.getin.exception.GeneralException; @@ -11,7 +12,10 @@ import com.bm.getin.service.PlaceService; import com.querydsl.core.types.Predicate; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.querydsl.binding.QuerydslPredicate; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -46,17 +50,25 @@ public ModelAndView adminPlaces(@QuerydslPredicate(root = Place.class) Predicate )); } - @GetMapping("/places/{placesId}") - public ModelAndView adminPlacesDetail(@PathVariable Long placesId) { - PlaceResponse place = placeService.getPlace(placesId) + @GetMapping("/places/{placeId}") + public ModelAndView adminPlaceDetail( + @PathVariable Long placeId, + @PageableDefault Pageable pageable + ) { + PlaceResponse place = placeService.getPlace(placeId) .map(PlaceResponse::from) .orElseThrow(() -> new GeneralException(ErrorCode.NOT_FOUND)); - - return new ModelAndView("admin/place-detail", Map.of( - "adminOperationStatus", AdminOperationStatus.MODIFY, - "place", place, - "placeTypeOption", PlaceType.values() - )); + Page events = eventService.getEvent(placeId, pageable); + + return new ModelAndView( + "admin/place-detail", + Map.of( + "adminOperationStatus", AdminOperationStatus.MODIFY, + "place", place, + "events", events, + "placeTypeOption", PlaceType.values() + ) + ); } @GetMapping("/places/new") @@ -140,6 +152,22 @@ public String deleteEvent( return "redirect:/admin/confirm"; } + @GetMapping("/events") + public ModelAndView adminEvents(@QuerydslPredicate(root = Event.class) Predicate predicate) { + List events = eventService.getEvents(predicate) + .stream() + .map(EventResponse::from) + .toList(); + + return new ModelAndView( + "admin/events", + Map.of( + "events", events, + "eventStatusOption", EventStatus.values() + ) + ); + } + @GetMapping("/events/{eventId}") public ModelAndView adminEventDetail(@PathVariable Long eventId) { EventResponse event = eventService.getEvent(eventId) diff --git a/src/main/java/com/bm/getin/dto/EventViewResponse.java b/src/main/java/com/bm/getin/dto/EventViewResponse.java index 3a2f98d..a21dc5d 100644 --- a/src/main/java/com/bm/getin/dto/EventViewResponse.java +++ b/src/main/java/com/bm/getin/dto/EventViewResponse.java @@ -66,4 +66,4 @@ public static EventViewResponse from(EventDto eventDto) { ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/bm/getin/repository/EventRepository.java b/src/main/java/com/bm/getin/repository/EventRepository.java index a1b512e..6e3e4c5 100644 --- a/src/main/java/com/bm/getin/repository/EventRepository.java +++ b/src/main/java/com/bm/getin/repository/EventRepository.java @@ -1,10 +1,13 @@ package com.bm.getin.repository; import com.bm.getin.domain.Event; +import com.bm.getin.domain.Place; import com.bm.getin.domain.QEvent; import com.bm.getin.repository.querydsl.EventRepositoryCustom; import com.querydsl.core.types.dsl.ComparableExpression; import com.querydsl.core.types.dsl.StringExpression; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; @@ -16,6 +19,8 @@ public interface EventRepository extends QuerydslPredicateExecutor, QuerydslBinderCustomizer { + Page findByPlace(Place place, Pageable pageable); + @Override default void customize(QuerydslBindings bindings, QEvent root) { bindings.excludeUnlistedProperties(true); diff --git a/src/main/java/com/bm/getin/service/EventService.java b/src/main/java/com/bm/getin/service/EventService.java index 0573060..1d84ed0 100644 --- a/src/main/java/com/bm/getin/service/EventService.java +++ b/src/main/java/com/bm/getin/service/EventService.java @@ -2,6 +2,7 @@ import com.bm.getin.constant.ErrorCode; import com.bm.getin.constant.EventStatus; +import com.bm.getin.domain.Event; import com.bm.getin.domain.Place; import com.bm.getin.dto.EventDto; import com.bm.getin.dto.EventViewResponse; @@ -11,6 +12,7 @@ import com.querydsl.core.types.Predicate; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -70,6 +72,25 @@ public Optional getEvent(Long eventId) { } } + @Transactional(readOnly = true) + public Page getEvent(Long placeId, Pageable pageable) { + try { + Place place = placeRepository.getReferenceById(placeId); + Page eventPage = eventRepository.findByPlace(place, pageable); + + return new PageImpl<>( + eventPage.getContent() + .stream() + .map(event -> EventViewResponse.from(EventDto.of(event))) + .toList(), + eventPage.getPageable(), + eventPage.getTotalElements() + ); + } catch (Exception e) { + throw new GeneralException(ErrorCode.DATA_ACCESS_ERROR, e); + } + } + public boolean upsertEvent(EventDto eventDto) { try { if (eventDto.id() != null) { diff --git a/src/main/resources/templates/admin/place-detail.html b/src/main/resources/templates/admin/place-detail.html index 2875188..2facd91 100644 --- a/src/main/resources/templates/admin/place-detail.html +++ b/src/main/resources/templates/admin/place-detail.html @@ -49,7 +49,26 @@
- + + + + + + + + + + + + + + + + + + + +
이벤트명이벤트 상태시작 ~ 종료현재 인원 / 최대 인원상세
테스트 이벤트OPENED1/1 9AM ~ 1/1 12PM0 / 10상세
\ No newline at end of file diff --git a/src/main/resources/templates/admin/place-detail.th.xml b/src/main/resources/templates/admin/place-detail.th.xml index 0d4a4a9..e6318c1 100644 --- a/src/main/resources/templates/admin/place-detail.th.xml +++ b/src/main/resources/templates/admin/place-detail.th.xml @@ -16,5 +16,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/bm/getin/controller/AdminControllerTest.java b/src/test/java/com/bm/getin/controller/AdminControllerTest.java index 65f4c6d..99cb9c6 100644 --- a/src/test/java/com/bm/getin/controller/AdminControllerTest.java +++ b/src/test/java/com/bm/getin/controller/AdminControllerTest.java @@ -13,6 +13,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; @@ -27,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -76,10 +79,11 @@ void givenQueryParams_whenRequestingAdminPlacesPage_thenReturnsAdminPlacesPage() @Test void givenPlaceId_whenRequestingAdminPlaceDetailPage_thenReturnsAdminPlaceDetailPage() throws Exception { // Given - long placeId = 1L; + Long placeId = 1L; given(placeService.getPlace(placeId)).willReturn(Optional.of( PlaceDto.of(placeId, null, null, null, null, null, null, null, null) )); + given(eventService.getEvent(eq(placeId), any(PageRequest.class))).willReturn(Page.empty()); // When & Then mvc.perform(get("/admin/places/" + placeId)) @@ -88,17 +92,19 @@ void givenPlaceId_whenRequestingAdminPlaceDetailPage_thenReturnsAdminPlaceDetail .andExpect(view().name("admin/place-detail")) .andExpect(model().hasNoErrors()) .andExpect(model().attributeExists("place")) + .andExpect(model().attributeExists("events")) .andExpect(model().attribute("adminOperationStatus", AdminOperationStatus.MODIFY)) .andExpect(model().attribute("placeTypeOption", PlaceType.values())); then(placeService).should().getPlace(placeId); + then(eventService).should().getEvent(eq(placeId), any(PageRequest.class)); } @DisplayName("[view][GET] 어드민 페이지 - 장소 세부 정보 뷰, 데이터 없음") @Test void givenNonexistentPlaceId_whenRequestingAdminPlaceDetailPage_thenReturnsErrorPage() throws Exception { // Given - long placeId = 1L; + Long placeId = 1L; given(placeService.getPlace(placeId)).willReturn(Optional.empty()); // When @@ -109,6 +115,7 @@ void givenNonexistentPlaceId_whenRequestingAdminPlaceDetailPage_thenReturnsError // Then then(placeService).should().getPlace(placeId); + then(eventService).shouldHaveNoInteractions(); } @DisplayName("[view][GET] 어드민 페이지 - 장소 새로 만들기 뷰") @@ -140,7 +147,7 @@ void givenNewPlace_whenSavingPlace_thenSavesPlaceAndReturnsToListPage() throws E 10, null ); - given(placeService.createPlace(placeRequest.toDto())).willReturn(true); + given(placeService.upsertPlace(placeRequest.toDto())).willReturn(true); // When mvc.perform(post("/admin/places") @@ -155,7 +162,7 @@ void givenNewPlace_whenSavingPlace_thenSavesPlaceAndReturnsToListPage() throws E .andDo(MockMvcResultHandlers.print()); // Then - then(placeService).should().createPlace(placeRequest.toDto()); + then(placeService).should().upsertPlace(placeRequest.toDto()); } @DisplayName("[view][GET] 어드민 페이지 - 이벤트 리스트 뷰") @@ -248,7 +255,7 @@ void givenNewEvent_whenSavingEvent_thenSavesEventAndReturnsToListPage() throws E // Given long placeId = 1L; EventRequest eventRequest = EventRequest.of(null,"test event", EventStatus.OPENED, LocalDateTime.now(), LocalDateTime.now(), 10, 10, null); - given(eventService.createEvent(eventRequest.toDto(PlaceDto.idOnly(placeId)))).willReturn(true); + given(eventService.upsertEvent(eventRequest.toDto(PlaceDto.idOnly(placeId)))).willReturn(true); // When & Then mvc.perform( @@ -262,7 +269,7 @@ void givenNewEvent_whenSavingEvent_thenSavesEventAndReturnsToListPage() throws E .andExpect(flash().attribute("adminOperationStatus", AdminOperationStatus.CREATE)) .andExpect(flash().attribute("redirectUrl", "/admin/places/" + placeId)) .andDo(MockMvcResultHandlers.print()); - then(eventService).should().createEvent(eventRequest.toDto(PlaceDto.idOnly(placeId))); + then(eventService).should().upsertEvent(eventRequest.toDto(PlaceDto.idOnly(placeId))); } @DisplayName("[view][GET] 어드민 페이지 - 기능 확인 페이지") diff --git a/src/test/java/com/bm/getin/service/EventServiceTest.java b/src/test/java/com/bm/getin/service/EventServiceTest.java index 6b6f0b1..115c3db 100644 --- a/src/test/java/com/bm/getin/service/EventServiceTest.java +++ b/src/test/java/com/bm/getin/service/EventServiceTest.java @@ -6,6 +6,7 @@ import com.bm.getin.domain.Event; import com.bm.getin.domain.Place; import com.bm.getin.dto.EventDto; +import com.bm.getin.dto.EventViewResponse; import com.bm.getin.exception.GeneralException; import com.bm.getin.repository.EventRepository; import com.bm.getin.repository.PlaceRepository; @@ -17,6 +18,9 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.test.util.ReflectionTestUtils; import javax.persistence.EntityNotFoundException; @@ -73,6 +77,24 @@ void givenDataRelatedException_whenSearchingEvents_thenThrowsGeneralException() then(eventRepository).should().findAll(any(Predicate.class)); } + @DisplayName("이벤트 뷰 데이터를 검색하면, 페이징된 결과를 출력하여 보여준다.") + @Test + void givenNothing_whenSearchingEventViewResponse_thenReturnsEventViewResponsePage() { + // Given + given(eventRepository.findEventViewPageBySearchParams(null, null, null, null, null, PageRequest.ofSize(10))) + .willReturn(new PageImpl<>(List.of( + EventViewResponse.from(EventDto.of(createEvent("오전 운동", true))), + EventViewResponse.from(EventDto.of(createEvent("오후 운동", false))) + ))); + + // When + Page list = sut.getEventViewResponse(null, null, null, null, null, PageRequest.ofSize(10)); + + // Then + assertThat(list).hasSize(2); + then(eventRepository).should().findEventViewPageBySearchParams(null, null, null, null, null, PageRequest.ofSize(10)); + } + @DisplayName("이벤트 ID로 존재하는 이벤트를 조회하면, 해당 이벤트 정보를 출력하여 보여준다.") @Test void givenEventId_whenSearchingExistingEvent_thenReturnsEvent() { @@ -121,6 +143,24 @@ void givenDataRelatedException_whenSearchingEvent_thenThrowsGeneralException() { then(eventRepository).should().findById(any()); } + @DisplayName("이벤트 ID로 존재하는 이벤트를 조회하면, 해당 이벤트 정보를 출력하여 보여준다.") + @Test + void givenPlaceIdAndPageable_whenSearchingEventsWithPlace_thenReturnsEventsPage() { + // Given + long placeId = 1L; + Place place = createPlace(); + given(placeRepository.getReferenceById(placeId)).willReturn(place); + given(eventRepository.findByPlace(place, PageRequest.ofSize(5))).willReturn(Page.empty()); + + // When + Page result = sut.getEvent(placeId, PageRequest.ofSize(5)); + + // Then + assertThat(result).hasSize(0); + then(placeRepository).should().getReferenceById(placeId); + then(eventRepository).should().findByPlace(place, PageRequest.ofSize(5)); + } + @DisplayName("이벤트 정보를 주면, 이벤트를 생성하고 결과를 true 로 보여준다.") @Test void givenEvent_whenCreating_thenCreatesEventAndReturnsTrue() {