[SPRING]영화/리뷰 프로젝트 적용하기-(1)

59836 단어 MultipartMultipart

파일 업로드로 영화를 등록하고, 사용자들이 영화 리뷰를 기록하는 것을 예제로 작성해봅니다.

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

영화 등록처리

  • controller 패키지에는 movieController 클래스를 생성합니다.
  • 영화 등록에 사용할 '/movie/register' 을 추가해줍니다.
@Controller
@Log4j2
@RequestMapping("/movie")
public class MovieController {

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

}
  • templates에 movie 폴더를 추가해주고 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="Entier 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-breose="Browse"></label>
                </div>
            </div>
            <div class="box">

            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
<script>
    $(document).ready(function(e){

    }); //document ready
</script>
    </th:block>
</th:block>
</html>

  • MovieDTO/MovieImageDTO 클래스와 MovieService
  • 영화와 관련해서는 이미 엔티티 클래스 처리가 완료되었기 때문에 DTO와 서비스 계층을 구성해 주는 작업만 처리합니다.
  • MovieDTO는 Movie클래스를 기준으로 작성합니다.
  • MOvieDTO 클래스의 내부에는 업로드된 파일들의 정보를 포함해야 하므로 MovieImageDTO 클래스도 같이 추가합니다.
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {

    private Long mno;

    private String title;

    @Builder.Default
    private List<MovieImageDTO> imageDTOList = new ArrayList<>();
    //화면에 영화 이미지들도 같이 수집해서 전달해야 하므로 내부적으로 리스트를 이용하여 수집합니다.
}
  • 영화의 이미지를 의미한느 MovieImageDTO는 이전의 UploadResultDTO와 동일합니다.
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MovieImageDTO {

    private String uuid;

    private String imgName;

    private String path;

    public String getImageURL(){
        try{
            return URLEncoder.encode(path + File.separator + 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 클래스를 추가합니다.
  • 추가된 dtoToEntity()는 Map 타입으로 Movie객체와 MovieImage 객체의 리스트를 처리합니다.
public interface MovieService {

    Long register(MovieDTO movieDTO);

    default Map<String,Object> dtoToEntity(MovieDTO movieDTO){  //Map타입으로 반환
        Map<String,Object> entityMap = new HashMap<>();
        //key값을 String, Value값을 Object형으로 

        Movie movie = Movie.builder()
                .mno(movieDTO.getMno())
                .title(movieDTO.getTitle())
                .build();
        entityMap.put("movie",movie);
        //Map에 키,값 저장

        List<MovieImageDTO> imageDTOList = movieDTO.getImageDTOList();
        //MovieImageDTO 처리
        if(imageDTOList != null && imageDTOList.size() > 0){
        //리스트는 stream()으로 해결하는것이 편하다.
            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());//List로 변환
            entityMap.put("imageList",movieImageList);
            //Map에 키,값 저장
        }
        return entityMap;
    }
}
  • HashMap의 중요한 특성
    -HashMap은 또한 '복제 가능'및 '직렬화 가능'인터페이스를 구현합니다.
    -HashMap은 중복 값을 허용하지만 중복 키는 허용하지 않습니다.
    -HashMap은 또한 여러 널값을 허용하지만 널 키는 하나만 될 수 있습니다.
    -HashMap은 동기화되지 않으며 요소의 순서도 보장하지 않습니다.

  • MovieServiceImpl은 MovieRepository와 MovieImageRepository를 주입받도록 구성하고, dtoToEntity()에서 반환한 객체들을 이용해서 save()를 처리해줍니다.

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

    private final MovieRepository movieRepository;
    private final MovieImageRepository movieImageRepository;

    @Override
    @Transactional
    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");
        //Map에서 꺼내온 값들을 Movie,List<MovieImage>로 형변환
        
        movieRepository.save(movie);
        
        movieImageList.forEach(movieImage -> {
            movieImageRepository.save(movieImage);
        });
        
        return movie.getMno();
    }
}
  • MovieController 에서는 Post 방식으로 전달된 파라미터들을 MovieDTO로 수집하여 MovieService객체의 register()를 호출합니다
@Controller
@Log4j2
@RequestMapping("/movie")
@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";
    }
}
  • 첨부파일을 보여주는 영역 처리하기
  • 화면에서 가장 먼저 처리할 것은 첨부파일을 업로드 하는 것입니다.
  • 이 과정에서 업로드된 파일을 화면에 보여주는 부분도 같이 처리해 주어야합니다.
  • register.html에서 파일 업로드를 하는 부분의 클래스는 'custom-file-input'이므로 이를 이용하여 이벤트 처리를 해줍니다.
  • 화면에서 첨부파일의 업로드는 별도의 버튼 없이 파일을 선택하면 자동으로 이루어지도록하기 위해서 change 이벤트를 처리합니다.
<!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>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
        <script>
             $(document).ready(function(e){

                var regex =new RegExp("(.*?)\.(exe|sh|zip|alz|tiff)$");
                // .*? : 줄 바꿈 문자를 제외한 모든 문자를 0개 이상 찾되, 최소 크기로 찾는다.
                var maxSize = 10485760; //10MB

                function checkExtension(fileName, fileSize){

                    if(fileSize>maxSize){
                        alert("파일 사이즈 초과")
                        return false;
                    }
                    if(regex.test(fileName)){   //RegExp.test() => 패턴이 있는지 없는지를 테스트하는데 목적
                        alert("해당 종류의 파일은 업로드 할 수 없습니다.")
                        return false;
                    }
                    return true;
                }

                $(".custom-file-input").on("change",function(){

                    var fileName = $(this).val().split("\\").pop();
                    //pop() : 배열에서 마지막 요소를 제거하고 그 요소를 반환합니다.
                    $(this).siblings(".custom-file-label").addClass("selected").html(fileName);
                    //siblings() : 자신을 제외한 형제 요소를 찾습니다.
                    //addClass() : 새로운 클래스를 추가(이미 가지고 있는 클래스는 추가되지 않습니다.)
                    //html() : 요소안의 내용을 지우고 새로운 내용을 넣습니다.
                    var formData = new FormData();
                    var inputFile = $(this);
                    var files = inputFile[0].files;
                    var append = false;
                    console.log(formData);
                    console.log(inputFile);
                    console.log(files);
                    console.log(append);

                    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(!append){
                        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){
                            conlose.log(textStatus);
                        }
                    });//$.ajax
                });//end change event
    }); //document ready
        </script>
    </th:block>
</th:block>

  • 업로드가 정상적으로 처리된 후에는 화면에서 섬네일 이미지를 보여줄 영역을 이용하여 섬네일을 추가해주어야 합니다.
  • register.htmldml <form>태그아래 쪽에 다음과 같은 스타일과 <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>
  • Ajax의 호출 결과는 showResult()라는 별도의 함수로 처리해줍니다.
  • register.html일부를 수정하고 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);
                }
 //실제 업로드 부분
                    $.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){
                            conlose.log(textStatus);
                        }

  • 각 이미지는 <li>태그로 구성되는데 이때 ImageDTO에 필요한 속성들을 구성하게 됩니다.

좋은 웹페이지 즐겨찾기