코드로 배우는 스프링 부트 웹 프로젝트/MovieReview

MovieReview 프로젝트 (4) (영화 등록 Register dto / service / controller)

soohykeee 2023. 1. 9. 17:53
728x90

 

MovieReview 프로젝트 (3) (파일 업로드)

 

MovieReview 프로젝트 (3) (파일 업로드)

MovieReview 프로젝트 (2) (@Query, 효율적 Join) MovieReview 프로젝트 (2) (@Query, 효율적 Join) MovieReview 프로젝트 (1) (MovieReview 프로젝트 설정, 다대다, Entity, Repository, Test) MovieReview 프로젝트 (1) (MovieReview 프로

soohykeee.tistory.com

 

 

 


 

앞서 미리 해봤던 파일 업로드를 이용하여 영화를 등록할 수 있도록 할 것이다.

우선적으로 해당 프로젝트에서 구현해야 될 것들을 정리해보면 아래와 같다.

  • 영화의 등록과 수정에는 파일 업로드 기능을 활용하여 영화 포스터 등을 등록, 수정할 수 있도록 한다.
  • 회원은 기존 회원들이 존재한다 가정하고 DB에 존재하는 회원들을 이용한다.
  • 회원은 특정한 영화 조회 페이지에서 평점과 자신의 감상 리뷰를 등록할 수 있다.
  • 조회 화면에서 회원은 자신이 등록한 리뷰의 내용을 수정, 삭제할 수 있다.

 

 

해당 movieReview 프로젝트도 화면 처리를 위해 앞서 guestbook에서 추가해주었던 static 파일들과 layout 파일들을 resources 하위에 추가해준다. 

 

 


 

영화 등록 처리

controller 디렉토리 하위에 MovieController 클래스를 추가해준다. 

package com.example.moviereview.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;

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

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

    }
}

 

 

dto 디렉토리 하위에 MovieDTO, MovieImageDTO를 생성해준다.

Movie와 관련된 엔티티는 이미 처리가 완료되었기 때문에 DTO와 servie만 구성해주면 된다. MovieDTO는 Movie 클래스를 기준으로 작성한다. 

 

package com.example.moviereview.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<>();

}

 

package com.example.moviereview.dto;

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

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

@Data
@NoArgsConstructor
@AllArgsConstructor
@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 "";
    }

}

 

 

service 디렉토리 생성 후, 하위에 MovieService, MovieServiceImpl 을 생성해준다.

 

Movie 를 JPA로 처리하기 위해서는 MovieDTO를 Movie 객체로 변환해주어야 하므로 MovieService에 dtoToEntity()를 추가해준다. 여기서 주의해야할 점은 앞서 해봤던 dtoToEntity() 와 달리 Movie 객체뿐만 아니라 MovieImage 객체들도 같이 처리 된다는 것이다. 한번에 두가지의 객체를 변환해줘야 하므로 Map<> 을 이용하여 반환해줘야 한다.

또한 MovieImage의 경우는 다중의 이미지 파일이 존재할 수 있으므로 List를 이용해줘야 한다.

package com.example.moviereview.service;

import com.example.moviereview.dto.MovieDTO;
import com.example.moviereview.dto.MovieImageDTO;
import com.example.moviereview.entity.Movie;
import com.example.moviereview.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<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();

        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은 MovieRepository, MovieImageRepository를 주입받도록 해주고, 앞서 작성해준 dtoToEntity를 이용하여 map.get을 통해 저장된 movie, movieImage를 entity로 변환 후 repository에 저장해준다.

package com.example.moviereview.service;

import com.example.moviereview.dto.MovieDTO;
import com.example.moviereview.entity.Movie;
import com.example.moviereview.entity.MovieImage;
import com.example.moviereview.repository.MovieImageRepository;
import com.example.moviereview.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
@RequiredArgsConstructor
@Log4j2
public class MovieServiceImpl implements MovieService {

    private final MovieRepository movieRepository;
    private final MovieImageRepository movieImageRepository;

    @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 -> {
            movieImageRepository.save(movieImage);
        });

        return movie.getMno();
    }
}

 

 

 

다시 MovieController와 화면 처리 register.html을 수정해줘야한다.

MovieController에서는 POST방식으로 전달된 파라미터들을 MovieDTO로 수집해서 MovieService 타입 객체의 register()를 호출하도록 작성해야한다. list로 redirect 되는데, 이는 나중에 구현해줄 예정이다.

package com.example.moviereview.controller;

import com.example.moviereview.dto.MovieDTO;
import com.example.moviereview.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.servlet.mvc.support.RedirectAttributes;

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

    private final MovieService movieService;

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

    }

    @PostMapping("/register")
    public String register(MovieDTO movieDTO, RedirectAttributes redirectAttributes) {
        log.info("movieDTO : " + movieDTO);

        Long mno = movieService.register(movieDTO);

        redirectAttributes.addFlashAttribute("msg", mno);

        return "redirect:/movie/list";

    }
}

 

 

첨부파일이 업로드 되었을 경우 change 이벤트를 설정해주고, 업로드가 정상적으로 이루어졌을 경우 화면에 섬네일 이미지를 보여주기 위해서 <style> 태그를 이용하여 추가해준다. 또한 섬네일 이미지에서 x 표시 클릭 시 삭제 될 수 있도록 설정해준다. 

submit 버튼을 클릭했을 시, 다음과 같이 작업을 처리하도록 작성해줘야 한다.

  • 각 이미지 <li> 태그의 'data-' 속성들을 읽는다.
  • 읽어 들인 속성값을 이용해서 <form> 태그 내에 <input type='hidden'> 태그들을 생성한다.
  • <input type='hidden'>의 이름에는 'imageDTOList[0]'과 같이 인덱스 번호를 붙여 처리한다.

hidden 타입으로 값을 넣어줘야 보이지 않고 값들을 list로 변환하여 처리할 수 있다.

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

 

 

현재 작성한 파일 업로드의 순서는 다음과 같다.

  1. 파일 업로드가 되면 <li> 태그가 구성된다.
  2. Submit 버튼을 클릭하면 <form> 태그 내에 태그들이 생성된다.
  3. MovieController에서 POST 방식으로 전달된 데이터는 MovieImageDTO로 수집된다
  4. MovieService에서 MovieImageDTO들은 Movie 엔티티 객체 내에 MovieImage로 처리된다.
  5. JPA에 의해서 save() 처리 후, DB에 저장된다.

 

 

 


 

 

728x90