Skip to content

Commit

Permalink
Implement #18 - event list inside place detail page
Browse files Browse the repository at this point in the history
어드민 페이지에서 장소 상세 페이지마다 연결된 이벤트를
모아서 보여주는 기능 구현

###
이전 수정 기능 개발 시 upsert를 이용하였는데 누락된 부분이있어 추가함
  • Loading branch information
bmcho committed Aug 18, 2022
1 parent 00e3367 commit 6cbc74d
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 17 deletions.
46 changes: 37 additions & 9 deletions src/main/java/com/bm/getin/controller/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
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;
import com.bm.getin.service.EventService;
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;
Expand Down Expand Up @@ -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<EventViewResponse> 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")
Expand Down Expand Up @@ -140,6 +152,22 @@ public String deleteEvent(
return "redirect:/admin/confirm";
}

@GetMapping("/events")
public ModelAndView adminEvents(@QuerydslPredicate(root = Event.class) Predicate predicate) {
List<EventResponse> 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)
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/bm/getin/dto/EventViewResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ public static EventViewResponse from(EventDto eventDto) {
);
}

}
}
5 changes: 5 additions & 0 deletions src/main/java/com/bm/getin/repository/EventRepository.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,6 +19,8 @@ public interface EventRepository extends
QuerydslPredicateExecutor<Event>,
QuerydslBinderCustomizer<QEvent> {

Page<Event> findByPlace(Place place, Pageable pageable);

@Override
default void customize(QuerydslBindings bindings, QEvent root) {
bindings.excludeUnlistedProperties(true);
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/bm/getin/service/EventService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -70,6 +72,25 @@ public Optional<EventDto> getEvent(Long eventId) {
}
}

@Transactional(readOnly = true)
public Page<EventViewResponse> getEvent(Long placeId, Pageable pageable) {
try {
Place place = placeRepository.getReferenceById(placeId);
Page<Event> 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) {
Expand Down
21 changes: 20 additions & 1 deletion src/main/resources/templates/admin/place-detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,26 @@
<button id="removePlace" type="button">삭제</button>
<button id="backToPlaces" type="button">취소</button>
<hr>
<!-- TODO: 여기에 장소와 연관된 이벤트들이 리스트로 보이면 좋을 듯 -->
<table id="eventTable">
<thead>
<tr>
<th>이벤트명</th>
<th>이벤트 상태</th>
<th>시작 ~ 종료</th>
<th>현재 인원 / 최대 인원</th>
<th>상세</th>
</tr>
</thead>
<tbody>
<tr>
<td class="eventName">테스트 이벤트</td>
<td class="eventStatus">OPENED</td>
<td class="eventDatetime">1/1 9AM ~ 1/1 12PM</td>
<td class="people">0 / 10</td>
<td><a>상세</a></td>
</tr>
</tbody>
</table>
<button id="newEvent" type="button">새 이벤트</button>
</body>
</html>
12 changes: 12 additions & 0 deletions src/main/resources/templates/admin/place-detail.th.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,17 @@
<attr sel="#savePlace" th:form="placeForm" th:formaction="@{/admin/places}" th:formmethod="post" />
<attr sel="#removePlace" th:if="${place?.id} != null" th:onclick="'location.href=\'' + @{/admin/places/{placeId}/delete(placeId=${place?.id})} + '\''" />
<attr sel="#backToPlaces" th:onclick="'location.href=\'' + @{/admin/places} + '\''" />

<attr sel="#eventTable">
<attr sel="tbody" th:remove="all-but-first">
<attr sel="tr[0]" th:each="event : ${events}">
<attr sel="td.eventName" th:text="${event.eventName}" />
<attr sel="td.eventStatus" th:text="${event.eventStatus}" />
<attr sel="td.eventDatetime" th:text="${#temporals.format(event.eventStartDatetime, 'M/d ha', 'US')} + ' ~ ' + ${#temporals.format(event.eventEndDatetime, 'M/d ha', 'US')}" />
<attr sel="td.people" th:text="${event.currentNumberOfPeople} + '명 / ' + ${event.capacity} + '명'" />
<attr sel="td/a" th:text="'상세'" th:href="@{'/admin/events/' + ${event.id}}" />
</attr>
</attr>
</attr>
<attr sel="#newEvent" th:if="${place} != null" th:onclick="'location.href=\'' + @{/admin/places/{placeId}/newEvent(placeId=${place?.id})} + '\''" />
</thlogic>
19 changes: 13 additions & 6 deletions src/test/java/com/bm/getin/controller/AdminControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -109,6 +115,7 @@ void givenNonexistentPlaceId_whenRequestingAdminPlaceDetailPage_thenReturnsError

// Then
then(placeService).should().getPlace(placeId);
then(eventService).shouldHaveNoInteractions();
}

@DisplayName("[view][GET] 어드민 페이지 - 장소 새로 만들기 뷰")
Expand Down Expand Up @@ -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")
Expand All @@ -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] 어드민 페이지 - 이벤트 리스트 뷰")
Expand Down Expand Up @@ -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(
Expand All @@ -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] 어드민 페이지 - 기능 확인 페이지")
Expand Down
40 changes: 40 additions & 0 deletions src/test/java/com/bm/getin/service/EventServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<EventViewResponse> 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() {
Expand Down Expand Up @@ -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<EventViewResponse> 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() {
Expand Down

0 comments on commit 6cbc74d

Please sign in to comment.