[코드로 배우는 스프링부트 웹 프로젝트] - 영화 리스트 생성(1) : 등록 페이지 생성

Jongwon·2023년 1월 17일
0

앞서 배운 내용들을 응용하여 영화와 리뷰 리스트를 보여주는 페이지를 만들겠습니다.

먼저 이전에 사용하던 List 템플릿을 다시 static 폴더에 넣어 이용하겠습니다. 또한 별도로 생성한 Layout 타임리프 페이지는 templates 폴더에 넣어 이용하겠습니다.



영화 등록 화면부터 생성하겠습니다. MovieController를 생성하고, 등록 화면을 간단하게 보여줄 수 있도록 컨트롤러와 화면을 만들겠습니다.

MovieController

package org.zerock.mreview.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
@Log4j2
@RequestMapping("/movie")
public class MovieController {

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

    }
}

templates 아래에 movie/register.html을 생성하고 아래의 코드로 간단한 등록 화면을 생성합니다.
register.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 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="form-control custom-file-input files" id="fileInput" multiple>
                    <label class="custom-file-label" data-browse="Browse"></label>
                </div>
            </div>
            <div class="box"></div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>

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

            });
        </script>
    </th:block>
</th:block>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>

다음으로는 Controller에서 데이터를 받아 Service에 전달해야하기 때문에 DTO를 생성합니다.

MovieImageDTO

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

        return "";
    }
}

getImageURL()과 getThumbnailURL()은 이전에 사용했던 UploadResultDTO에 있는 메서드와 유사합니다. 타임리프 페이지에서 이미지의 주소를 지정해줄 때 사용할 예정입니다.

MovieDTO

package org.zerock.mreview.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를 통해 Movie가 가진 이미지 리스트를 가져옵니다.



서비스 계층을 생성합니다. MovieService와 MovieServiceImpl를 생성합니다.

MovieService

package org.zerock.mreview.service;

import org.zerock.mreview.dto.MovieDTO;
import org.zerock.mreview.dto.MovieImageDTO;
import org.zerock.mreview.entity.Movie;
import org.zerock.mreview.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()
                        .imgName(movieImageDTO.getImgName())
                        .path(movieImageDTO.getPath())
                        .uuid(movieImageDTO.getUuid())
                        .movie(movie)
                        .build();

                return movieImage;
            }).collect(Collectors.toList());
            
            entityMap.put("imgList", movieImageList);
        }
        
        return entityMap;
    }
}

entityMap에 Movie와 MovieImage 엔티티가 매핑되어 반환되는데, 이 매핑된 결과를 이용하여 서비스 구현에서 한번에 두 엔티티를 가져올 수 있습니다.

MovieServiceImpl

package org.zerock.mreview.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.zerock.mreview.dto.MovieDTO;
import org.zerock.mreview.entity.Movie;
import org.zerock.mreview.entity.MovieImage;
import org.zerock.mreview.repository.MovieImageRepository;
import org.zerock.mreview.repository.MovieRepository;

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

@Service
@Log4j2
@RequiredArgsConstructor
public class MovieServiceImpl implements MovieService {

    @Autowired
    private final MovieRepository movieRepository;

    @Autowired
    private final MovieImageRepository imageRepository;


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


POST 메서드가 실행되면 list페이지로 돌아가도록 Controller를 설계하였습니다.
MovieController

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


파일을 첨부했을 때, 바로 이미지가 서버에 업로드가 되도록하고, 이를 미리보기로 볼 수 있도록 설계하려고 합니다. 또한 삭제 버튼을 누르면 올라간 사진이 서버에서 삭제되도록 설계하겠습니다.

먼저 파일을 선택하면 자동으로 서버로 이미지가 업로드가 되도록 script를 수정하겠습니다.

register.html

...
<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;
            }
            
            if(!appended) {return;}
            
            for(var value of formData.values()) {
                console.log(value);
            }
            
            $.ajax({
                url: '/uploadAjax',
                processData: false,
                contentType: false,
                data: formData,
                type: 'POST',
                dataType: 'json',
                success: function(result) {
                    console.log(result);
                },
                error: function(jqXHR, textStatus, errorThrown) {
                    console.log(textStatus);
                }
            });
        });
    });
</script>

콘솔창을 확인하면 JSON으로 업로드된 formData를 확인할 수 있습니다. 그리고 이전에 만들었던 /uploadAjax를 거쳐 서버에 이미지와 썸네일이 저장되었습니다.


다음으로 썸네일을 보여줄 공간을 추가하기 위해 html의 form태그 아래에 style과 div를 추가합니다. 그리고 script에서 Ajax통신이 성공적으로 일어났다면 썸네일이 화면에 로딩되도록 스크립트를 수정합니다.
register.html

...
</form>
<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>

...

        $.ajax({
            url: '/uploadAjax',
            processData: false,
            contentType: false,
            data: formData,
            type: 'POST',
            dataType: 'json',
            success: function(result) {
               showResult(result);
            },
            error: function(jqXHR, textStatus, errorThrown) {
                console.log(textStatus);
            }
        });
    });

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



X버튼을 눌렀을 때 삭제가 되도록 해야합니다. 서버에서도 삭제가 되도록 비동기적으로 처리하도록 합니다.
script의 showResult 함수에 이어 삭제 이벤트를 추가합니다.

$(".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();
        }
    });
});

새로고침을 진행하면 서버에 이미 저장된 이미지가 삭제가 안되는데 이는 추후에 처리해볼 예정입니다.



마지막으로 Submit버튼을 눌렀을 때 등록이 되도록 처리해야합니다. li에 있는 이미지들의 data 태그를 통해 모든 속성들을 가져와야하고, 이를 만들어둔 box form에 hidden 형태로 input을 만들어 저장해둡니다. submit을 하면 input이 DTO를 이용해 Controller로 전달되어 서버에 저장될 수 있습니다.

아직 list 페이지는 만들어지지 않았으므로 실제 submit()은 진행하지 않고, hidden input이 생성되는 것만 확인하겠습니다.

script 하단에 submit event를 작성합니다.
register.html

$(".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') + "'>";

    });

    $(".box").html(str);

    //$("form").submit();
});

submit버튼을 누르면 hidden input이 추가된 것을 확인할 수 있습니다.

profile
Backend Engineer

0개의 댓글