본문 바로가기
Spring

[Spring] 파일 업로드(6) 비동기식으로 첨부파일 업로드

by moca7 2024. 10. 24.

 

 

 

ㅁ main.jsp

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
   
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>


<style>
  form > label {
    font-size: 12px;
    color: gray;
  }
</style>

</head>
<body>

    <script>
      $(function(){
       
        $("input[type=file]").on('change', function(evt){
         
          const files = evt.target.files; // FileList {0:File, 1:File, ...}
          console.log(files);
       
         
          let totalSize = 0;
         
          for(let i=0; i<files.length; i++){
            if(files[i].size > 10 * 1024 * 1024) {
              alert("첨부파일의 최대 크기는 10MB입니다.");
              evt.target.value = ""; // 선택한 파일 초기화
              return; // 10MB 초과라면 여기서 function 빠져나가기
            }
          }
         
         
          totalSize += files[i].size; // 선택한 전체 파일 용량 알아내기
         
          if(totalSize > 100 * 1024 * 1024) {
            alert("전체 첨부파일 최대 크기는 100MB입니다.");
            evt.target.value = ""; // 선택한 파일 초기화
            return; // function 빠져나가기
          }
         
        })
       
      })
    </script>


    <h2>1. 한 개의 첨부파일 업로드 테스트</h2>
    <form action="${ contextPath }/board/insert1.do" method="post" enctype="multipart/form-data">
      게시글 제목 : <input type="text" name="boardTitle"> <br>
      게시글 내용 : <textarea name="boardContent"></textarea> <br>
      첨부파일 : <input type="file" name="uploadFile"> <br>
      <label>첨부파일 사이즈는 10MB 이하여야 됩니다.</label> <br><br>
     
      <button type="submit">등록</button>
    </form>
   

    <h2>2. 다중 첨부파일 업로드 테스트</h2>
    <form action="${ contextPath }/board/insert2.do" method="post" enctype="multipart/form-data">
      게시글 제목 : <input type="text" name="boardTitle"> <br>
      게시글 내용 : <textarea name="boardContent"></textarea> <br>
      첨부파일 : <input type="file" name="uploadFile" multiple> <br>
      <label>각 첨부파일 사이즈는 10MB 이하, 총 100MB 이하여야 됩니다.</label> <br><br>
     
      <button type="submit">등록</button>
    </form>
   
   

    <h2>3. 비동기식으로 첨부파일 업로드 테스트</h2>
    <div id="async_text">
      게시글 제목 : <input type="text" id="title"> <br>
      게시글 내용 : <textarea id="content"></textarea> <br>
      첨부파일 : <input type="file" id="file"> <br><br>
     
      <button id="ajax_btn">등록</button>
    </div>
   
   
    <script>
      $(function(){
        $("#ajax_btn").on("click", function(){
         
          // ajax 게시글 등록 (요청시 전달값 : 제목, 내용, 첨부파일)
          // 첨부파일을 전달해야할 경우 자바스크립트의 FormData 객체(가상의 form요소다.)에 담아서 전달
          let formData = new FormData();
          formData.append("boardTitle", document.getElementById("title").value);
          formData.append("boardContent", document.getElementById("content").value);
          formData.append("uploadFile", document.getElementById("file").files[0]); // File객체 담기
         
          $.ajax({
            url: '${contextPath}/board/ajaxInsert.do',
            type: 'post', // 파일 넘어갈땐 항상 post 방식
            data: formData,
           
            processData: false, // processData : false 선언시 formData를 string으로 변환하지 않음.
            contentType: false, // contentType : false 선언시 multipart/form-data로 전송되게 하는 옵션.
            success: function(result){
              if(result == "SUCCESS"){
                alert("성공적으로 등록!");
              }else{
                alert("등록 실패!");
              }
            },
            error: function(){
             
            }
          })
         
        })
      })
    </script>
   
   
   

    <h2>4. 첨부파일 목록 조회</h2>

</body>
</html>
 

 

- <button id="ajax_btn">등록</button> 부분.

 

- 서블릿을 요청하지 않는다.

- form요소도 안만든다.

 

 

- button의 타입이 submit인게 아니고 그냥 id가 submit이다.

 

- document.getElementById("file")

- document.getElementById("file").files

- document.getElementById("file").files[0]

 

- 파일을 넘기지 않고 단순한 텍스트만 넘길 때도 FormData에 담아서 넘겨도 된다.

 

- 파일 전송시에는 추가적으로 processData, contentType 속성을 반드시 기술해야 한다.

텍스트를 넘길 때는 저 두 속성을 쓰지 않아도 된다.

- 파일 전송시에는 저 두 개의 속성을 false로 해야 잘 넘어간다.

- processData : false 선언시 formData를 string으로 변환하지 않음.

- contentType : false 선언시 multipart/form-data로 전송되게 하는 옵션.

 

- success 함수에 매개변수 result를 둬야 한다.

 

- 새로고침하는 방법이 여러가지가 있는데 location.reload();하면 스크롤이 이동되지않고 그 위치에서 새로고침이 진행된다.

 

 

 

 

 

ㅁ BoardController

 

 
package com.br.file.controller;

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

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.br.file.dto.AttachDto;
import com.br.file.dto.BoardDto;
import com.br.file.service.BoardService;
import com.br.file.util.FileUtil;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequestMapping("/board")
@RequiredArgsConstructor
@Controller
public class BoardController {

  private final BoardService boardService;
  private final FileUtil fileUtil;
 
 
  @PostMapping("/insert1.do")
  public String insertOneFileBoard(BoardDto board, MultipartFile uploadFile) {
   
    log.debug("board: {}", board);      // 잘 담겼는지 확인용 로그 출력
    log.debug("attach: {}", uploadFile);  // 잘 담겼는지 확인용 로그 출력
   
   
    AttachDto attach = null;
   
    if(uploadFile != null && !uploadFile.isEmpty()) { // 첨부파일이 존재할 경우 => 업로드
      // 전달된 파일 업로드 처리
     

      // FileUitl 클래스의 fileupload 메소드를 호출 (uploadFile을 넘기면서)
      Map<String, String> map = fileUtil.fileupload(uploadFile);
     
     
      // (4) db에 기록할 정보를 자바 객체(AttachDto)에 세팅
      attach = AttachDto.builder()
                .filePath(map.get("filePath"))
                .originalName(map.get("originalFilename"))
                .filesystemName(map.get("filesystemName"))
                .build();
     
    }
   


    int result = boardService.insertOneFileBoard(board, attach);
   
    if(result > 0) {
      log.debug("게시글 등록 성공");
    }else {
      log.debug("게시글 등록 실패");
    }

    return "redirect:/"; // 성공이든 실패든 메인페이지 요청
   
  }
 
 
 
 
 
  @PostMapping("/insert2.do")
  public String insertManyFileBoard(BoardDto board, List<MultipartFile> uploadFile) {
   
    List<AttachDto> list = new ArrayList<>();
   
   
    for(MultipartFile file : uploadFile) {
     
      if(file != null && !file.isEmpty()) { // 파일이 존재할 경우
       
        Map<String, String> map = fileUtil.fileupload(file);
        list.add(AttachDto.builder()
                  .filePath(map.get("filePath"))
                  .originalName(map.get("originalFilename"))
                  .filesystemName(map.get("filesystemName"))
                  .build());
      }
    }
   
   
    // 넘어온 첨부파일이 없었다면 list는 비어있는 상태다.
    int result = boardService.insertManyFileBoard(board, list);
   
    // 첨부파일이 없었다면 result는 board에만 insert되고 result는 1이다.
    // 첨부파일이 있었다면 첨부파일의 개수만큼
    if(list.isEmpty() && result == 1 || !list.isEmpty() && result == list.size()) {
      log.debug("다중 첨부파일 게시글 등록 성공");
    }else {
      log.debug("다중 첨부파일 게시글 등록 실패");
    }
   
   
    return "redirect:/"; // 성공이든 실패든 메인페이지 요청
  }
 
 
  @ResponseBody
  @PostMapping("/ajaxInsert.do")
  public String insertAjaxFileBoard(BoardDto board, MultipartFile uploadFile) {
   
 
    AttachDto attach = null;
   
    if(uploadFile != null && !uploadFile.isEmpty()) {
     
      Map<String, String> map = fileUtil.fileupload(uploadFile);
     
      attach = AttachDto.builder()
                .filePath(map.get("filePath"))
                .originalName(map.get("originalFilename"))
                .filesystemName(map.get("filesystemName"))
                .build();
    }
   
   
    int result = boardService.insertOneFileBoard(board, attach);
   
    if(result > 0) {
      log.debug("ajax 첨부파일 게시글 등록 성공");
      return "SUCCESS";
    }else {
      log.debug("ajax 첨부파일 게시글 등록 실패");
      return "FAIL";
    }
   
   
  }
 
 
}

 

 

- "SUCCESS"와 "FAIL" 문자열을 돌려준다.

그런데 기본적으로 응답 뷰로 인식해서 페이지를 찾기 때문에 404에러가 발생할 수 있다.

@ResponseBody 어노테이션을 메소드 위에 반드시 붙여줘야 한다.

- 만약 응답데이터가 한글이면 @PostMapping에 produces~

 

 

 

 

 

ㅁ 서버 실행 후 비동기식으로 첨부파일 업로드 테스트

 

 

 

 

- 제목과 내용만 입력하고 등록해본다. board에 제목21, 내용21이 insert된다.

- 제목, 내용, 첨부파일을 하나 선택하고 등록해본다. 

board에 제목22, 내용22가 insert된다.

attachment에도 첨부파일이 제대로 insert 된다.

C:\upload\20241024에도 첨부파일이 이름이 바뀌어서 저장된다.

 

- alert도 뜬다. 

 

 

======================================================================

 

 

ㅁ 애초에 공통모듈로 작성하는 FileUtil 클래스에서 AttachDto 객체 자체를 담아도 되는거 아님?

굳이 map에 담고 또 map에서 꺼내야 함?

- 사실 그래도 됨. 그렇게 해도됨. 반환형만 AttachDto로 바꾸고.

- 그런데 모듈화할때는 범용적으로 쓸 수 있는 코드로 작성해주는게 제일 좋다.

~하면 무조건 AttachDto에만 담겨서 반환된다.

그런데 데이터를 담는 객체가 BoardAttachDto가 될 수도 있고 NoticeAttachDto가 될 수도 있다.

- Map

- 모듈화 작업 코드에 있어서는 ~Dto를 갖다 박는건 좋지 않다.

 

- 지금은 AttachDto에 담지만 다른거에 담길 수도 있기 때문에 Map으로 반환했다.

 

 

======================================================================

 

 

ㅁ 트랜잭션 관련

 

- 코드만 잘 써두면 사용자가 뭔가 잘못해서 발생하는 exception들이 대부분 runtime exception이다.