SpringBoot with JPAプロジェクト(M:N)5.映画の登録、登録された文章のリストの処理


📚 勉強した本:コード学習を用いたSpring Boot Webプロジェクト
▼githubアドレス:https://github.com/qkralswl689/LearnFromCode/tree/main/mreview2022

1.応用映画/評論項目

  • 映画(Movie)の登録と修正は
  • を含む.
  • メンバーは既存のメンバーが存在すると仮定し、DBのメンバーは
  • を使用する.
  • 会員は、特定の映画閲覧ページで採点と自分の鑑賞記録をコメント
  • として記録することができる.
  • クエリー画面では、メンバーは、自分が記録したコメント内容
  • を変更/削除することができる.

    2.映画登録処理


    2-1.DTOクラスの作成


    MovieDTOはMovieクラスに基づいて作成され、MovieDTOはスクリーン上で同時に映画画像を収集し、伝達する必要があるため、内部でリストを使用して収集される.
    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<>();
    }
    

    2-2.MovieImageDTOクラスの作成


    MovieImageDTOクラスも追加されます.MovieDTOクラスにはアップロードファイルの情報が含まれている必要があります.
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.io.UnsupportedEncodingException;
    import java.net.URLEncoder;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class MovieImageDTO {
    
        private String uuid;
    
        private String imgName;
    
        private String path;
    
        public String getImageURL(){
    
            try {
                return URLEncoder.encode(path + "/" + uuid + "_" + imgName,"UTF-8");
    
            }catch (UnsupportedEncodingException e){
                e.printStackTrace();
            }
            return "";
        }
    
        public String getThumbnailURL(){
            try {
                return URLEncoder.encode(path+"/s_" + uuid + "_" +imgName ,"UTF-8");
            }catch (UnsupportedEncodingException e){
                e.printStackTrace();
            }
            return "";
        }
    }

    2-3.サービスインタフェースの作成


    MovieDTOは、MovieをJPAとして扱うためにMovieオブジェクトに変換する必要があるため、MovieServiceにdtoEntity()を追加します.
  • 注意点
    ->ムービーオブジェクトとムービーイメージオブジェクトを一緒に処理!一度に2種類のオブジェクトを返さなければならないので、Mapタイプを使用して返されます.
    =>追加されたdtoEntity()は、MapタイプでMovieオブジェクトとMovieImageオブジェクトのリスト
  • を処理する.
    package com.example.mreview2022.service;
    
    import com.example.mreview2022.dto.MovieDTO;
    import com.example.mreview2022.dto.MovieImageDTO;
    import com.example.mreview2022.entity.Movie;
    import com.example.mreview2022.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);
    
        default Map<String, Object> dtoToEntity(MovieDTO movieDTO) { //Map 타입으로 변환
    
            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;
        }
    }

    2-4.ServiceImplクラスの作成


    dtoEntity()を使用して返されるオブジェクト処理save()
    package com.example.mreview2022.service;
    
    import com.example.mreview2022.dto.MovieDTO;
    import com.example.mreview2022.entity.Movie;
    import com.example.mreview2022.entity.MovieImage;
    import com.example.mreview2022.repository.MovieImageRepository;
    import com.example.mreview2022.repository.MovieRepository;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    
    import javax.transaction.Transactional;
    import java.util.List;
    import java.util.Map;
    
    @Service
    @RequiredArgsConstructor
    public class MovieServiceImpl implements MovieService {
    
        private final MovieRepository movieRepository; // final
    
        private final MovieImageRepository imageRepository; // final
    
        @Transactional
        @Override
        public Long register(MovieDTO movieDTO) {
    
            Map<String,Object> entityMap = dtoToEntity(movieDTO);
            Movie movie = (Movie) entityMap.get("movie");
            List<MovieImage> movieImageList = (List<MovieImage>) entityMap.get("imgList");
    
            movieRepository.save(movie);
    
            movieImageList.forEach(movieImage -> {
                imageRepository.save(movieImage);
            });
    
            return movie.getMno();
        }
    }

    2-5.コントローラの作成


    MovieControllerは、MovieServiceタイプオブジェクトのregister()を呼び出すために、POST方式で渡されるパラメータをMovieDTOに収集する
    *カタログページは後ほど記入
    package com.example.mreview2022.controller;
    
    import com.example.mreview2022.dto.MovieDTO;
    import com.example.mreview2022.service.MovieService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.servlet.mvc.support.RedirectAttributes;
    
    @Controller
    @RequestMapping("/movie")
    @RequiredArgsConstructor
    public class MovieController {
    
        private final MovieService movieService; //final
    
        @GetMapping("/register")
        public void register(){
    
        }
    
        @PostMapping("/register")
        public String register(MovieDTO movieDTO, RedirectAttributes redirectAttributes){
    
            Long mno = movieService.register(movieDTO);
    
            redirectAttributes.addFlashAttribute("msg",mno);
    
            return "redirect:/movie/list";
        }
    }

    2-6.Htmlの作成


    画面上のSubmitボタンをクリックしたときのタスクの処理順序
    1)各画像liタグのdata-属性を読み出す
    2)取得した属性値を使用してフォームタグ内にinputtype="hidden"タグを生成する
    3)inputtype="hidden"の名前にインデックス番号を付けて処理
    <!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 Register Page</h1>
    
            <form th:action="@{/movie/register}" th:method="post"  >
                <div class="form-group">
                    <label >Title</label>
                    <input type="text" class="form-control" name="title" placeholder="Enter Title">
                </div>
    
                <div class="form-group fileForm">
                    <label >Image Files</label>
                    <div class="custom-file">
                        <input type="file"  class="custom-file-input files" id="fileInput" multiple>
                        <label class="custom-file-label" data-browse="Browse"></label>
                    </div>
                </div>
    
                <div class="box">
    
                </div>
    
                <style>
                    .uploadResult {
                        width: 100%;
                        background-color: gray;
                        margin-top: 10px;
                    }
    
                    .uploadResult ul {
                        display: flex;
                        flex-flow: row;
                        justify-content: center;
                        align-items: center;
                        vertical-align: top;
                        overflow: auto;
                    }
    
                    .uploadResult ul li {
                        list-style: none;
                        padding: 10px;
                        margin-left: 2em;
                    }
    
                    .uploadResult ul li img {
                        width: 100px;
                    }
                </style>
    
                <div class="uploadResult">
                    <ul>
    
                    </ul>
                </div>
                <button type="submit" class="btn btn-primary">Submit</button>
            </form>
    
            <script>
        $(document).ready(function(e) {
    
            var regex = new RegExp("(.*?)\.(exe|sh|zip|alz|tiff)$");
            var maxSize = 10485760; //10MB
    
            function checkExtension(fileName, fileSize){
    
                if(fileSize >= maxSize){
                    alert("파일 사이즈 초과");
                    return false;
                }
    
                if(regex.test(fileName)){
                    alert("해당 종류의 파일은 업로드할 수 없습니다.");
                    return false;
                }
                return true;
            }
    
            $(".custom-file-input").on("change", function() {
    
                var fileName = $(this).val().split("\\").pop();
                $(this).siblings(".custom-file-label").addClass("selected").html(fileName);
    
                var formData = new FormData();
    
                var inputFile = $(this);
    
                var files = inputFile[0].files;
    
                var appended = false;
    
                for (var i = 0; i < files.length; i++) {
    
                    if(!checkExtension(files[i].name, files[i].size) ){
                        return false;
                    }
    
                    console.log(files[i]);
                    formData.append("uploadFiles", files[i]);
                    appended = true;
                }
    
                //upload를 하지 않는다.
                if (!appended) {return;}
    
                for (var value of formData.values()) {
                    console.log(value);
                }
    
                //실제 업로드 부분
                //upload ajax
                $.ajax({
                    url: '/uploadAjax',
                    processData: false,
                    contentType: false,
                    data: formData,
                    type: 'POST',
                    dataType:'json',
                    success: function(result){
                        console.log(result);
                        showResult(result);
                    },
                    error: function(jqXHR, textStatus, errorThrown){
                        console.log(textStatus);
                    }
                }); //$.ajax
            }); //end change event
    
    
            function showResult(uploadResultArr){
    
                var uploadUL = $(".uploadResult ul");
    
                var str ="";
    
                $(uploadResultArr).each(function(i, obj) {
    
                    str += "<li data-name='" + obj.fileName + "' data-path='"+obj.folderPath+"' data-uuid='"+obj.uuid+"'>";
                    str + " <div>";
                    str += "<button type='button' data-file=\'" + obj.imageURL + "\' "
                    str += "class='btn-warning btn-sm'>X</button><br>";
                    str += "<img src='/display?fileName=" + obj.thumbnailURL + "'>";
                    str += "</div>";
                    str + "</li>";
                });
    
                uploadUL.append(str);
            }
    
            $(".uploadResult ").on("click", "li button", function(e){
    
                console.log("delete file");
    
                var targetFile = $(this).data("file");
    
                var targetLi = $(this).closest("li");
    
                $.ajax({
                    url: '/removeFile',
                    data: {fileName: targetFile},
                    dataType:'text',
                    type: 'POST',
                    success: function(result){
                        alert(result);
    
                        targetLi.remove();
                    }
                }); //$.ajax
            });
    
    
            //prevent submit
            $(".btn-primary").on("click", function(e) {
                e.preventDefault();
    
                var str = "";
    
                $(".uploadResult li").each(function(i,obj){
                    var target = $(obj);
    
                    str += "<input type='hidden' name='imageDTOList["+i+"].imgName' value='"+target.data('name') +"'>";
    
                    str += "<input type='hidden' name='imageDTOList["+i+"].path' value='"+target.data('path')+"'>";
    
                    str += "<input type='hidden' name='imageDTOList["+i+"].uuid' value='"+target.data('uuid')+"'>";
    
                });
    
                //태그들이 추가된 것을 확인한 후에 comment를 제거
                $(".box").html(str);
    
                $("form").submit();
    
            });
    
    
    
        }); //document ready
    </script>
    
        </th:block>
    
    </th:block>
  • 運転画面
    現在リスト画面が実現していませんが、Submitボタンをクリックするとエラー画面が表示されますが、DBを検索すると保存がわかります
    -プロセス全体
    1)ファイルアップロード後、liラベルが設定されます
    2)Submitボタンをクリックしてフォームラベル内にラベルを作成する
    3)MovieControllerでPOST方式で伝送されたデータはMovieImageDTOで収集される
    4)MovieServiceでは、MovieImageDTOはMovieエンティティオブジェクト内のMovieImageとみなされる
    5)JPAにてsave()処理を行いDBに保存する

  • 3.リスト処理と平均採点


    3-1.ページ処理


    N:1プロジェクトで使用するPageRequestDTOとPageResultDTOを追加

    3-2.ムービーDTOの変更


    MovieServiceのgetList()は、Movie、MovieImage、Double、Longをオブジェクトとする[]をリストに並べる形式です
    各オブジェクト[]をMovieDTOという名前のオブジェクトとして処理する必要があります.MovieDTOにはDoubleタイプのスコア平均とコメント処理個数のパラメータと日付に関連する部分も追加されています.
    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;
    
        private LocalDateTime regDate;
    
        private LocalDateTime modDate;
    }

    3-3.サービスの変更


    JPAで生成されたエンティティオブジェクトと、Double、Long等値をMovieDTOに変換したエンティティsToDto()を追加し、コントローラ呼び出し時に使用するgetList()を追加します.
  • entitiesToDto()が受信したパラメータ.
    ->Movieエンティティ、Listエンティティ、Doubleタイプの平均スコア、Longタイプのコメント数
  • リストエンティティリスト:
  • クエリー画面で複数のMovieImageを処理するために使用される理由
    
    import com.example.mreview2022.dto.MovieDTO;
    import com.example.mreview2022.dto.MovieImageDTO;
    import com.example.mreview2022.dto.PageRequestDTO;
    import com.example.mreview2022.dto.PageResultDTO;
    import com.example.mreview2022.entity.Movie;
    import com.example.mreview2022.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);
    
        PageResultDTO<MovieDTO,Object[]> getList(PageRequestDTO requestDTO); // 목록처리
    
        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;
        }
    
    }
    

    3-4.ServiceImplの変更

    import com.example.mreview2022.dto.MovieDTO;
    import com.example.mreview2022.dto.PageRequestDTO;
    import com.example.mreview2022.dto.PageResultDTO;
    import com.example.mreview2022.entity.Movie;
    import com.example.mreview2022.entity.MovieImage;
    import com.example.mreview2022.repository.MovieImageRepository;
    import com.example.mreview2022.repository.MovieRepository;
    import lombok.RequiredArgsConstructor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.domain.Sort;
    import org.springframework.stereotype.Service;
    
    import javax.transaction.Transactional;
    import java.util.Arrays;
    import java.util.List;
    import java.util.Map;
    import java.util.function.Function;
    
    @Service
    @RequiredArgsConstructor
    public class MovieServiceImpl implements MovieService {
    
        @Autowired
        private final MovieRepository movieRepository; // final
    
        @Autowired
        private final MovieImageRepository imageRepository; // final
        
        //... 생략
    
        @Override
        public PageResultDTO<MovieDTO, Object[]> getList(PageRequestDTO requestDTO) {
    
            Pageable pageable = requestDTO.getPageable(Sort.by("mno").descending());
    
            Page<Object[]> result = movieRepository.getListPage(pageable);
    
            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);
        }
    
    }

    3-6.コントローラの変更

    import com.example.mreview2022.dto.MovieDTO;
    import com.example.mreview2022.dto.PageRequestDTO;
    import com.example.mreview2022.service.MovieService;
    import lombok.RequiredArgsConstructor;
    import org.jetbrains.annotations.NotNull;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.servlet.mvc.support.RedirectAttributes;
    
    @Controller
    @RequestMapping("/movie")
    @RequiredArgsConstructor
    public class MovieController {
    
        @Autowired
        private final MovieService movieService; //final
    
    	//... 생략
    
        @GetMapping("/list")
        public void list(PageRequestDTO pageRequestDTO, @NotNull Model model){
    
            model.addAttribute("result",movieService.getList(pageRequestDTO));
        }
    }

    3-6.htmlの変更


    リストから出力されたデータがモデルに含まれているため、モデル結果を使用して出力されます.
    <!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>
    
            <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>
    
    
            <script th:inline="javascript">
    
            </script>
        </th:block>
    
    </th:block>
  • 運転結果