영화 등록, 사용자들이 영화 리뷰를 기록하기 위한 기능
- 영화 등록과 수정에는 영화 포스터를 등록할 수 있도록 구성
- 회원은 기존 회원들이 존재한다고 가정 후, 데이터베이스에 존재하는 회원을 이용
- 회원은 특정한 영화 조회 페이지에서 평점과 느낌을 리뷰로 기록가능
- 조회 화면에서 회원이 자신이 기록한 리뷰의 내용르 수정/삭제 가능
진행 순서
- 영화 등록 처리
- 영화 이미지 삭제 처리
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에 추가된다.
https://ismydream.tistory.com/m/94
jQuery closest() 메서드
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
[Spring 부트 - 영화 리뷰 프로젝트] 5. 영화 조회 처리 (0) | 2022.10.18 |
---|---|
[Spring 부트 - 영화 리뷰 프로젝트] 4. 영화 목록 처리 (0) | 2022.10.18 |
[Spring 부트 - 영화 리뷰 프로젝트] 2. 파일 업로드 처리 (2) 섬네일 이미지를 통한 화면 처리와 파일 삭제 (0) | 2022.10.18 |
[Spring 부트 - 영화 리뷰 프로젝트] 2. 파일 업로드 처리 (1) Ajax를 통한 JSON으로 이미지 업로드 (0) | 2022.10.18 |
[Spring 부트 - 영화 리뷰 프로젝트] 1. M:N (다대다) 관계 설계와 구현 [+ N+1 문제와 엔티티의 특정 속성 로딩 방법] (0) | 2022.10.17 |