본문 바로가기

Server Programming/Spring Boot Backend Programming

[Spring 부트 - 영화 리뷰 프로젝트] 4. 영화 목록 처리

반응형

영화 목록 처리

  1. 목록 처리와 함께 평균 평점 화면에 출력
  2.  

영화 목록 처리

  1. 페이징을 위한 DTO 추가
    : PageRequestDTO, PageResultDTO
  2. 서비스계층에서 getList() 메서드를 생성하기 위해, 각 Object[]을 MovieDTO 하나의 객체로 처리하기 위해
    : MovieDTO에 Double타입의 평점 평균과 리뷰의 개수 처리하는 파라미터와 날짜 관련 부분 추가
  3.  MovieRepository에 저장된 영화와 평점 엔티티의 처리를 위해
    : Object[]로 반환하는 getListPage() 메서드 이용해, MovieService, MovieServiceImpl 클래스 수정
    1. Movie, MovieImage 객체
    2. double값으로 나오는 평균 평점
    3. Long타입의 리뷰 개수
  4. MovieController에 list() 메서드 추가하고, 목록 화면 구현

 

 


1. 페이징을 위한 DTO 추가

 

PageRequesDTO

package com.movie.boot4.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;


//화면에 전달되는 게시물 목록 처리
@Builder
@AllArgsConstructor
@Data
public class PageRequestDTO {
    private int page;
    private int size;

    //화면에 전달되는 목록을 위한 생성자 : 1페이지부터 시작하고, 10페이지 단위로 목록 처리를 의미
    public PageRequestDTO(){
        this.page=1;
        this.size=10;
    }
    public Pageable getPageable(Sort sort){
        return PageRequest.of(page -1, size, sort);
        //정적으로 생성하기 위해 of를 사용하고, JPA에서 페이지는 0부터 시작하므로 page-1로 전달;
    }
}

 

PageResultDTO

package com.movie.boot4.dto;

import lombok.Data;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

//화면에 필요한 결과 게시물 목록
@Data

//DTO -> Entity로 변환하기 때문에
public class PageResultDTO<DTO, EN> {

    //DTO리스트
    private List<DTO> dtoList;

    //총 페이지 번호
    private int totalPage;

    //현재 페이지 번호
    private int page;
    //목록 사이즈
    private int size;

    //시작 페이지 번호, 끝 페이지 번호
    private int start, end;

    //이전, 다음
    private boolean prev, next;

    //페이지 번호  목록
    private List<Integer> pageList;


    //function 패키지에 있는 람다식으로 사용할 수 있는 편리한 함수형 메서드 :
    //매개변수와 반환값 존재 여부에 따라 구분 / 조건식 표현해 참 거짓 반환
    // Runnable -> run(), Supplier -> get(), Consumer -> accept(), Function -> apply() , Predicate -> test()

    //Page<Entity>의 객체들을 DTO객체로 변환해서 담는 메서드
    public PageResultDTO(Page<EN> result, Function<EN, DTO> fn){
        //페이징 결과를 스트림에 담아서, fn을 이용해 람다식을 통해 변환하고, 최종연산으로 리스트로 반환
        dtoList = result.stream().map(fn).collect(Collectors.toList());

        totalPage = result.getTotalPages();

        makePageList(result.getPageable());
    }//Function 함수형 인터페이스를 사용하면, 제네릭으로 정의되어 있기 때문에 어떤 엔티티를 전달해도 재사용 가능


    private void makePageList(Pageable pageable){

        this.page = pageable.getPageNumber() + 1; // JPA가 전달한 페이지는 0부터 시작하므로 1을 추가
        this.size = pageable.getPageSize();

        //임시 끝 번호
        int tempEnd = (int)(Math.ceil(page/10.0)) * 10;

        start = tempEnd - 9;

        prev = start > 1;

        //끝 번호 처리 : 임시 끝 번호와 크기 비교해 실제 마지막 번호와 임시 끝번호 중 선택
        end = totalPage > tempEnd ? tempEnd: totalPage;

        next = totalPage > tempEnd;


        //페이지 리스트
        //기본형 int스트림을 스트림으로 변환하는 메서드 : mapToObj()와 boxed()
        //int스트림을 이용해 범위 제한해 데이터를 추출하고, 다시 Stream<Integer>로 변환 후, 최종연산으로 담아서 리스트로 변환
        pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
        //pageList는 List<Integer>
    }

}

2. MovieDTO에 Double타입의 평점 평균과 리뷰의 개수 처리하는 파라미터와 날짜 관련 부분 추가

 

변경 전의 MovieDTO

package com.movie.boot4.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {
    private Long mno;
    private String title;

    //특정 필드를 특정 값으로 초기화하기위한 어노테이션
    @Builder.Default
    private List<MovieImageDTO> imageDTOList=new ArrayList<>();
    //영화 이미지도 같이 수집해 전달하므로, 내부에 리스트를 이용해 수집한다.
}

 

변경 후의, MovieDTO

package com.movie.boot4.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {
    private Long mno;
    private String title;

    //특정 필드를 특정 값으로 초기화하기위한 어노테이션
    @Builder.Default
    private List<MovieImageDTO> imageDTOList=new ArrayList<>();
    //영화 이미지도 같이 수집해 전달하므로, 내부에 리스트를 이용해 수집한다.
    
    //영화의 평균 평점
    private double avg;
    
    //리뷰 수 jpa의 count()
    private int reviewCnt;
    
    //BaseEntity에 속한 속성들
    private LocalDateTime regDate;
    private LocalDateTime modDate;
}

 

3. Object[]로 반환하는 getListPage() 메서드 이용해, 

MovieService, MovieServiceImpl 클래스 수정

 

(1) 엔티티 객체를 DTO로 변환하는 entitiesToDto() 

  • Movie 엔티티
  • List<MovieImage> 엔티티
    • 조회 화면에서 여러 개의 MovieImage를 처리하기 위해
  • Double 타입의 평점 평균
  • Long 타입의 리뷰 개수

(2) 컨트롤러 호출시 사용할 getList()과 getList() 구현

 

MovieService의 getList()

package com.movie.boot4.service;

import com.movie.boot4.dto.MovieDTO;
import com.movie.boot4.dto.MovieImageDTO;
import com.movie.boot4.dto.PageRequestDTO;
import com.movie.boot4.dto.PageResultDTO;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.MovieImage;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public interface MovieService {

    //영화 등록을 위한 메서드
    Long register(MovieDTO movieDTO);

    //영화를 JPA로 처리하기위한 메서드
    //: Movie와 함께 포함된 MovieImage를 함께 반환 -> Map 이용
    default Map<String, Object> dtoToEntity(MovieDTO movieDTO){
        //movie, movie 객체
        //imgList, List<MovieImageDTO>
        Map<String, Object> entityMap = new HashMap<>();

        Movie movie = Movie.builder()
                .mno(movieDTO.getMno())
                .title(movieDTO.getTitle())
                .build();

        entityMap.put("movie", movie);

        List<MovieImageDTO> imageDTOList = movieDTO.getImageDTOList();

        //MovieImageDTO 처리
        //: 이미지가 존재할 경우에
        if(imageDTOList !=null && imageDTOList.size()>0){
            List<MovieImage> movieImageList=imageDTOList.stream()
                    .map(movieImageDTO -> {
                       MovieImage movieImage=MovieImage.builder()
                               .path(movieImageDTO.getPath())
                               .imgName(movieImageDTO.getImgName())
                               .uuid(movieImageDTO.getUuid())
                               .movie(movie)
                               .build();
                       return movieImage;
                    }).collect(Collectors.toList());

            entityMap.put("imgList",movieImageList);
        }
        return entityMap;
    }

    //getListPage()를 이용해 목록 처리하는 메서드
    //: PageRequestDTO를 받아, PageResultDTO로 변환하는데, MovieDTO를 이용해 Object[]로 반환
    PageResultDTO<MovieDTO, Object[]> getList(PageRequestDTO requestDTO);
    
    //엔티티를 DTO로 변환하는 메서드
    default MovieDTO entitiesToDTO(Movie movie, List<MovieImage> movieImages, Double avg, Long reviewCnt){
        MovieDTO movieDTO = MovieDTO.builder()
                .mno(movie.getMno())
                .title(movie.getTitle())
                .regDate(movie.getRegDate())
                .modDate(movie.getModDate())
                .build();
        List<MovieImageDTO> movieImageDTOList = movieImages.stream()
                .map(movieImage -> {
                    return MovieImageDTO.builder()
                            .imgName(movieImage.getImgName())
                            .path(movieImage.getPath())
                            .uuid(movieImage.getUuid())
                            .build();
                }).collect(Collectors.toList());
        movieDTO.setImageDTOList(movieImageDTOList);
        movieDTO.setAvg(avg);
        movieDTO.setReviewCnt(reviewCnt.intValue());
        
        return movieDTO;
    }

}

 

MovieServiceImple의 getList() 구현

package com.movie.boot4.service;

import com.movie.boot4.dto.MovieDTO;
import com.movie.boot4.dto.PageRequestDTO;
import com.movie.boot4.dto.PageResultDTO;
import com.movie.boot4.entity.Movie;
import com.movie.boot4.entity.MovieImage;
import com.movie.boot4.repository.MovieImageRepository;
import com.movie.boot4.repository.MovieRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

@Service
@Log4j2
//final을 항상 의존성 주입하는 어노테이션
@RequiredArgsConstructor
public class MovieServiceImpl implements MovieService{
    private final MovieRepository movieRepository;
    private final MovieImageRepository imageRepository;


    //영화 객체와 영화 이미지 객체 모두 함꼐 등록해야하기 때문에
    @Transactional
    @Override
    public Long register(MovieDTO movieDTO){
        Map<String, Object> entityMap= dtoToEntity(movieDTO);
        //Map에 저장된 movie 객체
        Movie movie = (Movie) entityMap.get("movie");
        //Map에 저장된 imglist 리스트
        List<MovieImage> movieImageList=(List<MovieImage>) entityMap.get("imgList");

        //영화 엔티티 저장 후
        movieRepository.save(movie);

        //forEach문과 스트림을 이용해
        //: 영화 이미지 엔티티 저장
        movieImageList.forEach(movieImage -> {
            imageRepository.save(movieImage);
        });

        return null;
    }

    //getList() 구현
    @Override
    public PageResultDTO<MovieDTO, Object[]> getList(PageRequestDTO requestDTO){
        //requestDTO에서 페이징 메서드로 mno기준 정렬 수행해 페이징 객체에 저장
        Pageable pageable = requestDTO.getPageable(Sort.by("mno")
                .descending());

        //페이징 객체를 전달해 영화 목록을 받아서 Page에 배열로 저장
        Page<Object[]> result = movieRepository.getListPage(pageable);

        //배열에 있는 값으로 엔티티를 가져와 DTO 만들기
        Function<Object[], MovieDTO>fn =(arr ->
                entitiesToDTO(
                        (Movie) arr[0],
                        (List<MovieImage>) (Arrays.asList((MovieImage)arr[1])),
                        (Double) arr[2],
                        (Long) arr[3])
                );
        return new PageResultDTO<>(result, fn);
    }
}

4.MovieController에 list() 메서드 추가하고, 목록 화면 구현

 

(1) list() 메서드 작성

    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model){
        log.info("pageReQuestDTO : "+pageRequestDTO);
        
        model.addAttribute("result", movieService.getList(pageRequestDTO));
    }

(2) list.html 목록 화면 구현

  • 타임리프를 이용한 fragment 사용해 레이아웃 구성
  • h1클래스를 이용해 제목 구성하고, 등록 버튼 만들기
  • 목록을 위한 폼만들기
  • 테이블로 원하는 thead에 목록 컬럼 만들고
  • tbody에 반복문 이용해 목록 구성
    • 반복문 범위 <th scope: "row"> </th>
    • 타임리프의 if문을 이용해 이미지 있을 때만 출력 [리스트가 0보다 클때, 실제 이미지 존재할 때]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

    <th:block th:fragment="content">

        <h1 class="mt-4">Movie List Page
            <span>
                <a th:href="@{/movie/register}">
                    <button type="button" class="btn btn-outline-primary">REGISTER
                    </button>
                </a>
            </span>
        </h1>

        <form action="/movie/list" method="get" id="searchForm">
            <input type="hidden" name="page" value="1">
        </form>

        <table class="table table-striped">
            <thead>
            <tr>
                <th scope="col">#</th>
                <th scope="col">Picture</th>
                <th scope="col">Review Count</th>
                <th scope="col">AVG Rating</th>
                <th scope="col">Regdate</th>
            </tr>
            </thead>
            <tbody>

            <tr th:each="dto : ${result.dtoList}" >
                <th scope="row">
                    <a th:href="@{/movie/read(mno = ${dto.mno}, page= ${result.page})}">
                        [[${dto.mno}]]
                    </a>
                </th>
                <td><img th:if="${dto.imageDTOList.size() > 0 && dto.imageDTOList[0].path != null }"
                         th:src="|/display?fileName=${dto.imageDTOList[0].getThumbnailURL()}|" >[[${dto.title}]]</td>
                <td><b>[[${dto.reviewCnt}]]</b></td>
                <td><b>[[${dto.avg}]]</b></td>
                <td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
            </tr>

            </tbody>
        </table>



        <script th:inline="javascript">

        </script>
    </th:block>

</th:block>

 

페이징 만들기

   <ul class="pagination h-100 justify-content-center align-items-center">

            <li class="page-item " th:if="${result.prev}">
                <a class="page-link" th:href="@{/movie/list(page= ${result.start -1})}" tabindex="-1">Previous</a>
            </li>

            <li th:class=" 'page-item ' + ${result.page == page?'active':''} " th:each="page: ${result.pageList}">
                <a class="page-link" th:href="@{/movie/list(page = ${page})}">
                    [[${page}]]
                </a>
            </li>

            <li class="page-item" th:if="${result.next}">
                <a class="page-link" th:href="@{/movie/list(page= ${result.end + 1} )}">Next</a>
            </li>
        </ul>

 

+) 모달 창을 이용해 등록 후에 보여주는 작업 수행

반응형