본문 바로가기
개발입문/SPRING 게시판 만들기

[SPRING] 첨부파일 업로드

by 양히◡̈ 2022. 10. 14.

form을 submit하여 업로드하는 방식도 있지만, 이번에는 Ajax 방식을 이용해 구현해 볼 것이다.

먼저, web.xml에서 첨부파일 설정값을 설정해주어야 한다.
web.xml 에서 servlet 태그 안에 내용을 추가한다.

<!-- 첨부파일 설정 값 -->
<multipart-config>
    <location>c:\\upload\\temp</location>
    <max-file-size>20971520</max-file-size>
    <!-- byte 기본단위 1024 1024 1개 파일의 최대 업로드 용량은 20MB -->
    <max-request-size>41943040</max-request-size>
    <!-- 동시에 업로드하는 파일들의 총 크기 20971520 = 1024 * 1024 * 20(MB) -->
    <file-size-threshold>20971520</file-size-threshold>
    <!-- 스트림으로 처리할 수 있는 최대 크기를 초과하면 임시파일 저장 후 업로드 -->
</multipart-config>
 서블릿이 multipart를 인식하기 위해서 multipart-config를 설정해준다.

location 위치대로, c드라이브에 upload 폴더 생성 > 그 안에 temp 폴더를 생성한다.

 

다음으로, 스프링의 업로드 처리는 MultipartResolver라는 타입의 객체를 빈으로 등록해야 한다.

웹과 관련된 설정이므로, servlet-context.xml 을 이용해서 설정한다.

 

src > main > webapp > WEB-INF > spring > appServlet 내에 있는

servlet-context.xml 을 열어 맨 아래에 있는 </beans:bean> 코드 바로 위에 내용을 추가한다.

<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"></beans:bean>
<!-- 첨부파일 처리 -->

* Resolver : 어떤 요청이 왔을 때 요청을 받아 분배하여 처리하는 것

 

 

 

 

첨부파일 데이터 테이블 생성 및 매핑

이제 sql developer 에서 첨부파일용 테이블을 만들고 설정할 것이다.
admin 계정으로 접속해 쿼리문을 작성한다.

create table tbl_attach (
uuid varchar2(100) not null,
uploadpath varchar2(200) not null,
filename varchar2(100) not null,
filetype char(1) default 'I',
bno number(10,0) 
);

alter table tbl_attach add constraint pk_attach
primary key (uuid); --기본키 설정

alter table tbl_attach add constraint fk_board_attach
foreign key (bno)
references tbl_board(bno); --외래키 설정

commit;

 

 

STS 로 돌아와서, 첨부파일의 정보들을 저장하는 클래스를 생성한다.
src/main/java > kr.icia.domain > new Class 생성 > name: BoardAttachVO

package kr.icia.domain;

import lombok.Data;

@Data
public class BoardAttachVO {
	private String uuid; //중복방지용
	private String uploadPath;
	private String fileName;
	private boolean fileType;
	private Long bno;
}

 

BoardVO.java에도 변수를 추가한다.

private List<BoardAttachVO> attachList;

 

 

첨부파일용 새로운 인터페이스를 생성한다.

src/main/java > kr.icia.mapper > new Interface 생성 > name: BoardAttachMapper

package kr.icia.mapper;

import java.util.List;

import kr.icia.domain.BoardAttachVO;

public interface BoardAttachMapper {
	public void insert(BoardAttachVO vo); //첨부파일 등록
	
	public void delete(String uuid); //첨부파일 삭제
	
	public List<BoardAttachVO> findByBno(Long bno); //첨부파일 목록
	
	public void deleteAll(long bno); //첨부파일 여러개 한꺼번에 삭제
	
	public List<BoardAttachVO> getOldFiles();
	//111.zip커뮤니티를 사용하는 사용자들은 중복 파일명을 사용할 수도 있음
	//시스템은 동일 파일명에 대해서 내부적으로 저장하는 다른 이름을 가짐
}

 

 

데이터베이스를 매핑할 XML을 생성한다.

src/main/resources > kr.icia.mapper > new XML file 생성 > name: BoardAttachMapper.xml
BoardMapper.xml에서 dtd를 복사한 후 코드를 작성한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
 <mapper namespace="kr.icia.mapper.BoardAttachMapper">
 	<insert id = "insert">
 		insert into tbl_attach (uuid, uploadpath, filename, filetype, bno)
 		values (#{uuid},#{uploadPath},#{fileName},#{fileType},#{bno})
 	</insert>
 	
 	<delete id="delete">
 		delete from tbl_attach where uuid = #{uuid}
 	</delete>
 	
 	<select id="findByBno" resultType="kr.icia.domain.BoardAttachVO">
 		select * from tbl_attach where bno = #{bno}
 	</select>
 </mapper>

 

 

 

 

 

 

첨부파일 화면처리

다음으로, 글을 쓸 때 첨부가 가능하게 만들어야 하므로

register.jsp 의 div 맨 밑에 코드를 추가한다.

<!-- 첨부파일 처리 -->
<br />
<div class="row">
	<div class="col-lg-12">
		<div class="panel panel-default">
			<div class="panel-heading"></div>
			<div class="panel-body">
				<div class="form-group uploadDiv">
					파일 첨부: <input type="file" name="uploadFile" multiple>
				</div>
				<div class="uploadResult">
					<ul></ul>
				</div>
			</div>
		</div>
	</div>
</div>

서버를 구동해보면, 글쓰기 화면에서 파일첨부 버튼이 생성된 것을 볼 수 있다.

 

 

 

첨부파일 관련 클래스 생성

아직 눈에 보이는 버튼 부분만 구현한 것이므로, 이제 기능 부분도 구현해볼 것이다.

 

src/main/java > kr.icia.domain > new Class 생성 > name: AttachFileDTO

BoardAttachVO는 1대다 구조에서 어느 게시물의 첨부파일인지 처리하는 클래스였다면, AttachFileDTO는 첨부파일 1개에 대한 처리를 하는 클래스이다.

 

package kr.icia.domain;

import lombok.Data;

@Data
public class AttachFileDTO {
	private String uuid;
	private String uploadPath;
	private String fileName;
	private boolean image;
}

 

 

이제 새로운 첨부파일 컨트롤러를 만들 것이다.

src/main/java > kr.icia.controller > new Class 생성 > name: uploadController

package kr.icia.controller;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import kr.icia.domain.AttachFileDTO;
import lombok.extern.log4j.Log4j;

@Controller
@Log4j
public class uploadController {
	String uploadFolder = "c:\\upload";

	@PostMapping(value = "/uploadAjaxAction", produces = MediaType.APPLICATION_JSON_VALUE)
	@ResponseBody //응답을 json형태로 리턴
	public ResponseEntity<List<AttachFileDTO>> uploadAjaxPost(MultipartFile[] uploadFile) {
		//rest방식으로 ajax처리. 파일을 받고 json 값을 리턴한다
		
		List<AttachFileDTO> list = new ArrayList<>();
		//여러개 파일 저장을 위한 객체 배열 타입 선언

		String uploadFolderPath = getFolder();
		File uploadPath = new File(uploadFolder, uploadFolderPath);
		//예) c:\\upload\\2021\\04\\28에 파일 저장 예정
		
		if(uploadPath.exists() == false) {
			uploadPath.mkdirs();
		} //경로에 폴더들이 생성되어있지 않다면 생성해라
		
		//파일은 1개일수도, 여러개일 수도 있음
		for (MultipartFile multipartFile : uploadFile) {
			AttachFileDTO attachDTO = new AttachFileDTO();
			String uploadFileName = multipartFile.getOriginalFilename();
			//파일의 원래 이름 저장
			
			//익스플로러용 처리
			uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
			
			attachDTO.setFileName(uploadFileName); //파일이름저장
			
			UUID uuid = UUID.randomUUID(); //universal unique identifier, 범용 고유 식별자
			//파일의 중복을 회피
			
			uploadFileName = uuid.toString() + "_" + uploadFileName;
			//예) uuid_일일일.txt
			
			try {
				File saveFile = new File(uploadPath, uploadFileName);
				multipartFile.transferTo(saveFile);
				//서버에 파일 저장
				
				attachDTO.setUuid(uuid.toString());
				attachDTO.setUploadPath(uploadFolderPath);
				
				list.add(attachDTO);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return new ResponseEntity<>(list,HttpStatus.OK);

	}

	private String getFolder() {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		Date date = new Date();
		String str = sdf.format(date);
		return str.replace("-", File.separator);
		// 파일 검색 시간을 줄이기 위해서 폴더 1개에 모두 저장하는 것이 아니라 년월일로 구분하여 폴더를 생성하고 그 곳에 파일을 저장한다.
		// File.separator : 폴더 구분자를 운영체제에 맞춰서 변경한다.
	}
}

첨부파일을 저장할 때는 파일명이 중복되지 않도록 해야 하고, 한 폴더 내에 너무 많은 파일이 생성되지 않도록 고려해야 한다.

위의 문제를 방지하기 위해서 getFolder() 메소드를 이용해 년월일로 폴더를 생성하여 담을 수 있도록 만들고, UUID를 적용하여 파일 이름에 중복이 없도록 처리하고 있다.

 

 

 

 

첨부파일 버튼 스크립트

이제 페이지에서 파일을 첨부할 수 있도록 스크립트를 추가할 것이다. 파일 업로드에는 바이러스 감염 방지를 위해 일부 파일 확장자를 지정하여 파업로드를 제한하고, 파일 크기도 너무 큰 파일을 업로드하지 못하도록 걸러내는 작업을 포함해야 한다.

register.jsp 를 열고 하부에 <script>를 만든다.

<script>
	$(document).ready(function(e){
		var formObj=$("form[role='form']");
		$("button[type='submit']").on("click",function(e){
			e.preventDefault();
			console.log("submit clicked");
		});
		
		var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
		//정규 표현식. 일부 파일의 업로드 제한. https://regexper.com/
		
		var maxSize = 5242880; //5MB
		
		function checkExtension(fileName, fileSize) {
			if (fileSize >= maxSize){
				alert("파일 크기 초과");
				return false;
			}
			
			if (regex.test(fileName)) {
				alert("해당 종류의 파일은 업로드 불가.");
				return false;
			}
			return true;
		}
		
		$("input[type='file']").change(function(e) {
			//첨부파일 정보가 변경됐다면,
			var formData = new FormData();
			var inputFile = $("input[name='uploadFile']");
			var files = inputFile[0].files;
			//지정된 정보로 접근하여 배열 형태로 리턴
			for (var i = 0; i < files.length; i++){
				if (!checkExtension(files[i].name, files[i].size)){
					//확장자가 걸러내야할 경우라면 더 이상 처리하지 않고 종료
					return false;
				}
				formData.append("uploadFile", files[i]); //form에 관련 정보를 추가
			}
			$.ajax({
				url:'/uploadAjaxAction',
				processData:false,
				contentType:false,
				data:formData,
				type:'POST',
				dateType:'json',
				success:function(result){
					console.log(result);
				}
			});
		});
	});
</script>

여기서 첨부파일의 형식을 체크하기 위해 정규표현식(regExp)을 사용했다.

Reference ▶ 정규표현식 (Regex) 정리 (tistory.com)

 

서버 구동 후 글쓰기 화면에서 파일을 첨부할 수 있고, 파일이 첨부되면 아래처럼 콘솔창에 뜨게 된다.

지정된 경로에도 파일이 올라가고 있다.

 

 

 

첨부파일 목록 표시

현재는 첨부파일을 2개 이상 올렸을 때는 몇 개나 올렸는지 확인이 안 되고 있다.
이 부분을 보완하여 전체 목록이 표시되도록 수정할 것이다.

register.jsp 에서 첨부파일 업로드 함수인 function checkExtension() ajax의 success function 에 새로운 함수를 호출한다.

showUploadResult(result);

 

호출한 함수를 구현한다.

function showUploadResult(uploadResultArr) {
    if (!uploadResultArr || uploadResultArr.length == 0) {
        // json 처리 결과가 없다면 함수 종료.
        return;
    }
    var uploadUL = $(".uploadResult ul");
    var str = "";

    // each 구문은 전달된 배열의 길이 만큼, 
    // each 이후의 함수를 반복 처리.
    // https://api.jquery.com/jQuery.each/#jQuery-each-array-callback
    $(uploadResultArr)
            .each(
                    function(i, obj) {
                        var fileCallPath = encodeURIComponent(obj.uploadPath
                                + "/"
                                + obj.uuid
                                + "_"
                                + obj.fileName);
                        // encodeURIComponent : 
                        // uri 로 전달되는 특수문자의 치환.
                        // & ?
                        var fileLink = fileCallPath
                                .replace(new RegExp(
                                        /\\/g), "/");
                        // 전달되는 값들 중에서 역슬러시를 찾아서 슬러시로 변경.

                        str += "<li data-path='";
                       str += obj.uploadPath+"' data-uuid='";
                       str += obj.uuid+"' data-filename='";
                       str += obj.fileName+"' data-type='";
                       str += obj.image+"'><div>";
                        str += "<img src='/resources/img/attach.png' width='20' height='20'>";
                        str += "<span>" + obj.fileName
                                + "</span> ";
                        str += "<b data-file='"+fileCallPath;
                        str += "' data-type='file'>[x]</b>";
                        str += "</div></li>";
                    });
    uploadUL.append(str);
}// end_showUploadResult

Reference ▶ jQuery.each() | jQuery API Documentation

코드 작성 후, 첨부파일 아이콘을 attach.img를 명시한 resources/img 폴더 내에 넣는다.

attach.png
0.00MB

 

서버를 구동하고 첨부파일을 추가하면 파일 첨부 아래에 첨부된 파일의 목록이 보여진다.

 

 

 

 

첨부파일 업로드 취소 기능 구현

첨부파일을 올렸을 때 올리기를 취소하고 싶을 수가 있다.

이 부분을 버튼[x]은 만들어 놓았지만 기능은 아직 구현하지 않았기 때문에 이제 구현해볼 것이다.

 

첨부파일이 삭제되면 첨부파일을 백업해두었던 c드라이브에서도 삭제되어야 하고, 웹페이지의 첨부파일 목록에서도 삭제돼야 한다.

먼저 UploadController.Java 에 메소드를 추가한다.

//첨부파일 업로드 취소하기
@PostMapping("/deleteFile")
@ResponseBody
public ResponseEntity<String> deleteFile(String fileName, String type) {
    log.info("deleteFile: " + fileName);
    File file;

    try {
        file = new File("c:\\upload\\" +URLDecoder.decode(fileName, "UTF-8"));
        //한글의 경우 페이지 전환시 변경됨. 알맞는 문자 포맷으로 해석해서 읽어 들여야 함
        file.delete(); //파일삭제
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    return new ResponseEntity<String>("deleted", HttpStatus.OK); 
    //return null;
}

 

여기서 실제로 첨부파일을 삭제한 후에 콘솔에 fileName을 출력하도록 했는데, 실제로 표시되는 콘솔 로그를 보면 아래와 같을 것이다.

INFO : kr.icia.controller.uploadController - deleteFile: 2021%5C10%5C13%2F0064a823-8c6e-49f9-a08a-57796c378e53_%EC%8A%A4%ED%94%84%EB%A7%81%208%EC%9D%BC%EC%B0%A8.pdf

이 부분을 우리가 알아볼 수 있게 디코딩하는 작업을 거쳐야 한다.

실제로 디코딩하면 다음과 같이 변할 것이다.

INFO : kr.icia.controller.uploadController - deleteFile: 2021\10\13/0064a823-8c6e-49f9-a08a-57796c378e53_스프링 8일차.pdf

 

 

 

 

다음으로, 버튼에 기능을 넣기 위해 register.jsp 에 스크립트를 추가한다.

//올리기 취소 버튼
$(".uploadResult").on("click", "b", function(e) {
    console.log("delete file");

    var targetFile = $(this).data("file"); //fileCallPath: 저장경로+uuid+파일명.확장자
    var type = $(this).data("type"); //file
    var targetLi = $(this).closest("li"); //첨부파일 목록 지우기 위해 지정

    $.ajax({
        url : '/deleteFile',
        data : {
            fileName : targetFile,
            type : type
        },
        dataType : 'text',
        type : 'POST',
        success : function(result) {
            alert(result);
            targetLi.remove();
        }
    });
});

서버구동 후 글쓰기 화면으로 들어가 파일 첨부를 한 후에 [x]를 누르면 deleted 메세지와 함께 첨부파일 목록에서 삭제되고, 첨부파일이 저장되는 경로(c드라이브>upload>날짜폴더)에서도 파일이 정상적으로 삭제된 것을 확인할 수 있다.

 

 

 

글쓰기 완료시 첨부파일 정보를 DB에 저장하기

이후에 글을 읽었을 때도 첨부파일이 확인이 가능하도록 구현할 것인데, 그러려면 글쓰기에서 "전송"버튼을 클릭했을 때 첨부파일에 대한 정보도 같이 저장되어야 한다.

이 부분을 먼저 수정해볼 것이다.

register.jsp를 열고, 스크립트의 $("button[type='submit']").on("click", function(e) 함수 안에 내용을 추가한다.

$("button[type='submit']").on("click", function(e) {
    e.preventDefault();
    console.log("submit clicked");

    //글 등록버튼을 누르면 첨부파일 정보도 함께 전송됨
    var str="";
    $(".uploadResult ul li").each(function(i,obj) {
        var jobj = $(obj);
        console.dir(jobj); //console.dir(): JavaScript객체의 모든 속성을 보는 방법
        console.log("-----------------");
        console.log(jobj.data("fileName"));

        str += "<input type='hidden' name='attachList[";
        str += i + "].fileName' value='" + jobj.data("filename");
        str += "'>";

        str += "<input type='hidden' name='attachList[";
        str += i + "].uuid' value='" + jobj.data("uuid");
        str += "'>";

        str += "<input type='hidden' name='attachList[";
        str += i + "].uploadPath' value='" + jobj.data("path");
        str += "'>";

        str += "<input type='hidden' name='attachList[";
        str += i + "].fileType' value='" + jobj.data("type");
        str += "'>";
    });
    formObj.append(str).submit();
});

 

이제 글쓰기 화면에서 submit 버튼을 누르면 해당 첨부파일의 정보를 formObj에 같이 넣고 정보가 넘어가게 된다.

 

BoardController.java 에서 "/register" 부분도 수정해야 한다. 기존 내부에 있던 소스는 주석처리한다.

@PostMapping("/register")
public String register(BoardVO board, RedirectAttributes rttr) {
    if (board.getAttachList() != null) {
        board.getAttachList().forEach(attach -> log.info(attach));
    }
    //log.info("register : " + board);
    //service.register(board);
    //rttr.addFlashAttribute("result", board.getBno());
    return "redirect:/board/list";
}

기존에 log로 출력하던 board는 일단 중지하고, 첨부파일에 대한 요소를 로그로 출력하도록 하는 것이다.

 

서버를 구동하고 글쓰기 제출을 한 다음, 콘솔을 확인해보면 다음과 같이 콘솔창에 첨부파일 로그가 출력된다.

INFO : kr.icia.controller.BoardController - BoardAttachVO(uuid=1bb98eeb-97ce-4ff1-a647-06c20c42e58f, uploadPath=2021\10\13, fileName=스프링 10일차.pdf, fileType=false, bno=null)
INFO : kr.icia.controller.BoardController - BoardAttachVO(uuid=3d156ab1-c764-4390-a1a7-763b314f8913, uploadPath=2021\10\13, fileName=스프링 11일차.pdf, fileType=false, bno=null)

 

 

다음으로, BoardServiceImp.java 에 코드를 추가할 것이다.

먼저 기존 어노테이션을 사용하고 있던 @AllArgsConstructor 주석처리한다. (자동으로 만드는 것을 수동으로 바꿀 것이다.)

그리고 첨부파일 매퍼 클래스를 추가로 명시한다.

@Setter(onMethod_ = @Autowired)
private BoardAttachMapper attachMapper;

 

그리고 기존 register() 부분을 수정한다.

@Transactional 어노테이션을 추가하고, if구문과 forEach를 이용한 첨부파일을 삽입하는 코드를 추가했다.

@Transactional
@Override
public void register(BoardVO board) {
    log.info("register......" + board);
    mapper.insertSelectKey(board);

    if(board.getAttachList() == null || board.getAttachList().size() <=0) {
        return;
    }

    board.getAttachList().forEach(attach -> {attach.setBno(board.getBno());
    attachMapper.insert(attach);
    });
}

 

첨부파일이 없다면 게시물만 등록하고, 있다면 첨부파일도 같이 등록할 수 있도록 구성하는 것이다.

다시 BoardController.java 로 와서, 이전에 로그 출력하던 부분 주석을 다시 풀고 기존 거를 주석 처리한다.

잠시 로그 출력 방식을 바꿔서 확인해 보기 위한 작업이다.

@PostMapping("/register")
public String register(BoardVO board, RedirectAttributes rttr) {
    /*if (board.getAttachList() != null) {
        board.getAttachList().forEach(attach -> log.info(attach));
    }*/
    log.info("register : " + board);
    service.register(board);
    rttr.addFlashAttribute("result", board.getBno());
    return "redirect:/board/list";
}

일단 주석만 바꾼 후 서버구동을 해보고, 글쓰기도 테스트해볼 것이다.

그러면 이제 기존과는 달리 글작성 시 첨부파일 정보도 DB에 저장되고, sql developer에서도 확인이 가능하다.

 

 

댓글