본문 바로가기

Server Programming/Spring Boot Backend Programming

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

반응형

영화 등록, 사용자들이 영화 리뷰를 기록하기 위한 기능

  • 영화 등록과 수정에는 영화 포스터를 등록할 수 있도록 구성
  • 회원은 기존 회원들이 존재한다고 가정 후, 데이터베이스에 존재하는 회원을 이용
  • 회원은 특정한 영화 조회 페이지에서 평점과 느낌을 리뷰로 기록가능
  • 조회 화면에서 회원이 자신이 기록한 리뷰의 내용르 수정/삭제 가능

 

진행 순서

  1. 영화 등록 처리
  2. 영화 이미지 삭제 처리

1. 영화 등록 처리

 

(1) MovieController 작성

package com.movie.boot4.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
@RequestMapping("/movie")
@Log4j2
public class MovieController {

    @GetMapping("/register")
    public void register(){

    }
}

 

(2) register.html 작성

 

기본적인 영화 등록 페이지 구성

: 타임리프를 통한 fragment이용해 레이아웃 구성

<!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>


        <script>
        $(document).ready(function(e) {
        
        }); //document ready
</script> 

    </th:block>

</th:block>

 

(3) MovieDTO / MovieImageDTO 클래스와 MovieService 작성

: 영화 DTO와 서비스 계층 구성

 

-> MovieDTO에는 업로드된 파일들의 정보를 포함하므로, MovieImageDTO 클래스도 작성해야한다.

 

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<>();
    //영화 이미지도 같이 수집해 전달하므로, 내부에 리스트를 이용해 수집한다.
}

 

MovieImageDTO

:UploadResultDTO와 동일한 클래스로 getImageURL()과 getThumbnailURL()은 타임리프로 출력할 때 사용한다.

package com.movie.boot4.dto;

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+"/"+uuid+"_"+imgName,"UTF-8");
        }
        catch (UnsupportedEncodingException e){
            e.printStackTrace();
        }
        
        return "";
    }
}

 

MovieService 작성

(1) 영화 등록을 위한 register 메서드

(2) 영화 객체를 엔티티로 변환하기 위한 dtoToEntity 메서드

: 영화 엔티티에는 영화 이미지 리스트도 포함되어있으므로, Map을 통해 작성

package com.movie.boot4.service;

import com.movie.boot4.dto.MovieDTO;
import com.movie.boot4.dto.MovieImageDTO;
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;
    }

}

 

변경 전의 MovieServiceImpl

package com.movie.boot4.service;

import com.movie.boot4.dto.MovieDTO;
import com.movie.boot4.repository.MovieImageRepository;
import com.movie.boot4.repository.MovieRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

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

    
    @Override
    public Long register(MovieDTO movieDTO){
        return null;
    }
}

 

변경 후의 MovieServiceImpl

package com.movie.boot4.service;

import com.movie.boot4.dto.MovieDTO;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

@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;
    }
}

(4) MovieController와 화면 처리

: POST 방식으로 전달된 파라미터를 MovieDTO로 수집해 MovieService 타입 객체의 register()를 호출하도록 작성

 

MovieController 클래스

package com.movie.boot4.controller;

import com.movie.boot4.dto.MovieDTO;
import com.movie.boot4.service.MovieService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
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.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping("/movie")
@Log4j2
@RequiredArgsConstructor
//final 객체 의존성 주입 어노테이션
public class MovieController {

    private final MovieService movieService;

    @GetMapping("/register")
    public void register(){

    }

    //전달된 파라미터를 MovieDTO로 수집해 MovieService 타입 객체의 등록메서드 호출
    @PostMapping("/register")
    public String register(MovieDTO movieDTO, RedirectAttributes redirectAttributes){
        log.info("movieDTO : "+movieDTO );
        Long mno = movieService.register(movieDTO);

        redirectAttributes.addFlashAttribute("msg",mno);
        //리다이렉트 시에만 데이터 전달
        //addAttribute는 GET 방식이며 페이지를 새로고침 한다 해도 값이 유지된다.
        //addFlashAttribute는 POST 방식이며 이름처럼 일회성 데이터라 새로고침 하면 값이 사라진다.

        return "redirect:/movie/list";
    }
}

: 영화 등록 후, 목록 페이지로 이동하도록 변경필요

 

(5) 등록한 영화 화면 처리 : 업로드와 삭제

 

첨부파일 업로드

: 업로드된 파일을 화면에 보여주는 부분 처리

 

-> register.html에서 'custom-file-input'클래스를 이용해 이벤트 처리

[화면에서 별도의 버튼 없이 파일 선택시 첨부파일 업로드하도록 change 이벤트 처리]

 

첨부파일 업로드 함수 작성시 고려사항

1. 확장자 체크

2. 용량 체크

<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

        }); //document ready
        </script>

 

섬네일 이미지를 보여줄 영역 설정 후, 섬네일 이미지 추가

      <!-- 섬네일 이미지 영역 설정과 섬네일 이미지 추가 부분-->
            <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>

 

Ajax 호출 결과를 보여주는 showResult() 추가

-> 결과 데이터 반환시 showResult() 호출

        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);
        }

: 해당 이미지들은 <li> 태그로 구성되고, ImageDTO에 필요한 속성을 구성한다.

->따라서 속성들을 <form> 태그의 submit 실행시 내용물로 만들어 전송

<li data-name = "극한직업3.jpg" data-path="2020/10/15" data-uuid="e12290a3-94e0-4c96-8522-f38c8fb6b897e">

: 이미지 파일 선택시 자동 업로드

 

 

이미지 파일의 삭제와 submit 처리

: 함수와 Ajax

  //이미지 파일의 삭제와 submit 처리
        $(".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
        });

: 업로드 파일의 x버튼 클릭시 실제 서버에서도 섬네일과 원본파일 함께 삭제

 

 

업로드한 이미지 등록 처리

     //업로드한 이미지 등록 처리
       //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();

       });

: submit 클릭시 <from> 태그 안의 <div class='box'> 내의 hidden 타입 태그를 만든다.

-> 모든 내용 만들어진 후에 submit() 실행시 db에 추가된다.

 

 

 

 

 

 

 

 

반응형