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>
현재 작성한 파일 업로드의 순서는 다음과 같다.
- 파일 업로드가 되면 <li> 태그가 구성된다.
- Submit 버튼을 클릭하면 <form> 태그 내에 태그들이 생성된다.
- MovieController에서 POST 방식으로 전달된 데이터는 MovieImageDTO로 수집된다
- MovieService에서 MovieImageDTO들은 Movie 엔티티 객체 내에 MovieImage로 처리된다.
- JPA에 의해서 save() 처리 후, DB에 저장된다.