[34] 파일 업로드 처리 - 프로젝트의 첨부파일 - 등록

기존에 작성해 두었던 게시판 프로젝트에서 필요한 패키지와 리소스 등을 복사하여 ex05에 붙여넣기하고 프로젝트가 잘 실행되는지 확인한다!

 

pom.xml, web.xml, servlet-context.xml, root-context.xml 에서 빠진게 없는지 잘 확인한다.

 

 

1. 첨부파일 정보를 위한 준비

 

첨부파일이 게시물과 합쳐지면 가장 먼저 진행해야 하는 일은 게시물과 첨부파일의 관계를 저장하는 테이블의 설계가 우선이다. 게시물의 첨부파일은 각자 고유한 UUID를 가지고 있기 때문에 별도의 PK를 지정할 필요는 없지만, 게시물을 등록할 때 첨부파일 테이블 역시 같이 insert 작업이 진행되어야 하므로 트랜잭션 처리가 필요하다.

 

첨부파일을 보관하는 테이블은 tbl_attach로 설계한다. tbl_board는 tbl_reply와 이미 외래키의 관계를 가지고 있으므로 첨부파일이 추가되면 아래와 같은 구조가 된다. 

 

 

첨부파일의 보관은 UUID가 포함된 이름을 PK로 하는 uuid 칼럼과 실제 파일이 업로드된 경로를 의미하는 uploadPath, 파일 이름을 의미하는 fileName, 이미지 파일 여부를 판단할 수 있는 fileType, 해당 게시물 번호를 저장하는 bno 칼럼을 이용한다.

 

SQL을 처리하기 위해서는 파일 정보를 처리하기 위해 파라미터를 여러 개 사용해야 하는 불편함이 있으므로, org.zerock.domain 패키지에 아예 BoardAttachVO 클래스를 설계하는 것이 유용하다. (AttachFileDTO와 거의 유사하지만 게시물의 번호가 추가되었고, 혼란을 피하기 위해서 새로 클래스를 작성한다.)

 

- BoardAttachVO.java

package org.zerock.domain;

import lombok.Data;

@Data
public class BoardAttachVO {
	
	private String uuid;
	private String uploadPath;
	private String fileName;
	private boolean fileType;
	
	private Long bno;
}

 

기존의 BoardVO는 등록 시 한 번에 BoardAttachVO를 처리할 수 있도록 List<BoardAttachVO>를 추가한다. 

 

- BoardVO.java

 

1-1. 첨부파일 처리를 위한 Mapper 처리

 

첨부파일 정보를 데이터베이스를 이용해서 보관하므로 이를 처리하는 SQL을 Mapper 인터페이스와 XML을 작성해서 처리한다. 

 

- BoardAttachMapper 인터페이스 생성

package org.zerock.mapper;

import java.util.List;

import org.zerock.domain.BoardAttachVO;

public interface BoardAttachMapper {
	public void insert(BoardAttachVO vo);
	public void delete(String uuid);
	public List<BoardAttachVO> findByBno(Long bno);
}

 

 

BoardAttachMapper의 경우는 첨부파일의 수정이라는 개념이 존재하지 않기 때문에, insert()와 delete() 작업만을 처리한다. 특정 게시물의 번호로 첨부파일을 찾는 작업이 필요하므로 findByBno() 메서드를 정의한다.

 

Mapper 인터페이스의 SQL을 처리하는 BoardAttachMapper.xml을 추가한다. 

 

 - BoardAttachMapper.xml

<?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 = "org.zerock.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="org.zerock.domain.BoardAttachVO">
		select * from tbl_attach where bno = #{bno}
	</select>
</mapper>

 

 

2. 등록을 위한 화면 처리

 

첨부파일 자체의 처리는 Ajax를 통해서 이루어지므로, 게시물의 등록 시점에는 현재 서버에 업로드된 파일들에 정보를 등록하려는 게시물의 정보와 같이 전송해서 처리한다. 이 작업은 게시물의 등록 버튼을 클릭했을 때 현재 서버에 업로드된 파일의 정보를 <input type = 'hidden'>으로 만들어서 한 번에 전송하는 방식을 사용한다.

 

게시물의 등록을 담당하는 /board/register.jsp 파일에서 첨부파일을 추가할 수 있도록 수정하는 작업부터 시작한다. 

 

 - register.jsp의 일부

 

<!-- 첨부파일 -->
<div class = "row">
	<div class = "col-lg-12">
		<div class = "panel panel-default">
			<div class = "panel-heading">File Attach</div>
			<!-- /.panel-heading -->
			<div class = "panel-body">
				<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>
</div>

 

 

추가된 <div> 안에는 이전 예제에서 사용한 <div class = 'uploadResult'> 등을 추가해서 파일 업로드한 결과를 처리할 수 있도록 한다. 브라우저에서는 새로운 부분이 아래와 같이 추가되는 것을 확인한다. 

 

업로드를 위한 uploadAjax.jsp의 CSS 부분도 register.jsp 내에 추가한다. 

 

2-1. JavaScript 처리

 

복잡한 부분은 파일을 선택하거나 'Submit Button'을 클릭했을 때의 JavaScript 처리다. 가장 먼저 'Submit Button'을 클릭했을 때 첨부파일 관련된 처리를 할 수 있도록 기본 동작을 막는 작업부터 시작한다. 

 

 - register.jsp 의 일부

 

<script>
$(document).read(function(e){
	var formObj = $("form[role='form']");
	$("button[type='submit']").on("click", function(e){
		e.preventDefault();
		console.log("submit clicked");
	});
});
</script>

 

파일의 업로드는 별도의 업로드 버튼을 두지 않고, <input type = 'file'> 의 내용이 변경되는 것을 감지해서 처리하도록 한다. $(document).ready() 내에 파일 업로드 시 필요한 코드를 아래와 같이 추가한다.

 

 - register.jsp

<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)$");
	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]);
		}
		
		$.ajax({
			url : '/uploadAjaxAction',
			processData : false,
			contentType : false, 
			data : formData,
			type : 'POST',
			dataType : 'json',
				success:function(result){
					console.log(result);
				}
		});
	});
});
</script>

 

첨부된 파일의 처리는 기존과 동일하지만 아직은 섬네일이나 파일 아이콘을 보여주는 부분은 처리하지 않는다. 브라우저의 콘솔창을 이용해서 업로드가 정상적으로 처리되는지 만을 확인한다. 아래 화면은 첨부파일을 3개 추가하는 경우 업로드 결과를 콘솔창에서 확인한 모습이다. 

 

 

업로드된 결과를 화면에 섬네일 등을 만들어서 처리하는 부분은 별도의 showUploadResult() 함수를 제작하고 결과를 반영한다. 

 

function showUploadResult(uploadResultArr){
		  if(!uploadResultArr || uploadResultArr.length == 0){return ;}
		  var uploadUL = $(".uploadResult ul");
		  var str = "";
		  
		  $(uploadResultArr).each(function(i, obj){
			     
			   if(!obj.image){
		       
		       var fileCallPath =  encodeURIComponent( obj.uploadPath+"/"+ obj.uuid +"_"+obj.fileName);
		       
		       var fileLink = fileCallPath.replace(new RegExp(/\\/g), "/");
		       
		       str += "<li><a href='/download?fileName="+fileCallPath+"'><img src='/resources/img/attach.png'>"
		    		   +obj.fileName+"</a>" + "<span data-file=\ '"+ fileCallPath+"\' data-type='file'> x </span>"
		    		   + "<div></li>"

		     }else{
		       
		       var fileCallPath =  encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid +"_"+obj.fileName);
		       var originPath = obj.uploadPath+ "\\"+obj.uuid +"_"+obj.fileName;
		       originPath = originPath.replace(new RegExp(/\\/g),"/");
		       
		       str += "<li><a href=\"javascript:showImage(\'"
		    		   +originPath+"\')\"><img src='/display?fileName="+fileCallPath+"'></a>"
		    				   + "<span data-file=\'" + fileCallPath + "\' data-type='image'> x </span><li>";
		     }
		   });
			
			uploadResult.append(str);
	  }

 

위의 함수를 추가한 뒤,

$.ajax() 호출부분에 함수를 호출하는 코드를 넣는다.

 

 

이미지 파일인 경우와 일반 파일의 경우에 보여지는 화면의 내용은 showUploadResult() 내에 아래와 같은 HTML 태그들을 이용해서 작성한다. 

 

function showUploadResult(uploadResultArr){
		  if(!uploadResultArr || uploadResultArr.length == 0){return ;}
		  var uploadUL = $(".uploadResult ul");
		  var str = "";
		  
		  $(uploadResultArr).each(function(i, obj){
			  
			   //image type
		        if(obj.image){
		          var fileCallPath =  encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid +"_"+obj.fileName);
		          str += "<li><div>";
		          str += "<span> "+ obj.fileName+"</span>";
		          str += "<button type='button' data-file=\'"+fileCallPath+"\' data-type='image' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
		          str += "<img src='/display?fileName="+fileCallPath+"'>";
		          str += "</div>";
		          str +"</li>";
		        }else{
		          var fileCallPath =  encodeURIComponent( obj.uploadPath+"/"+ obj.uuid +"_"+obj.fileName);            
		            var fileLink = fileCallPath.replace(new RegExp(/\\/g),"/");
		              
		          str += "<li><div>";
		          str += "<span> "+ obj.fileName+"</span>";
		          str += "<button type='button' data-file=\'"+fileCallPath+"\' data-type='file' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
		          str += "<img src='/resources/img/attach.png'></a>";
		          str += "</div>";
		          str +"</li>";
		        } 
		  });
			uploadUL.append(str);
	  }

 

게시물 등록 화면에서 첨부파일이 업로드되면 아래와 같은 모습으로 보이게 된다. 

 

 

2-2. 첨부파일의 변경 처리

 

첨부파일의 변경은 사실상 업로드된 파일의 삭제이므로 'x' 모양의 아이콘을 클릭할 때 이루어지도록 이벤트를 처리한다.

 

 - register.jsp

	  $(".uploadResult").on("click", "button", function(e){
		  console.log("delete file");
	  });

 

업로드된 파일에 'x' 아이콘을 클릭하면 콘솔창에 'delete file'이 출력되는 것을 볼 수 있다. 

 

 

삭제를 위해서는 업로드된 파일의 경로와 UUID가 포함된 파일 이름이 필요하므로 앞서 작성된 부분을 수정한다. <button> 태그에 'data-file'과 'data-type' 정보를 추가한다. 

 

(아까 이미 추가된 코드를 넣었으므로 넘어가겠음 ㅎ)

 

'x' 아이콘을 클릭하면 서버에서 삭제하도록 이벤트를 처리한다. 

 

	  $(".uploadResult").on("click", "button", function(e){
		  console.log("delete file");
		  
		  var targetFile = $(this).data("file");
		  var type = $(this).data("type");
		  
		  var targetLi = $(this).closest("li");
		  
		  $.ajax({
		  	url : 'deleteFile',
		  	data : {fileName : targetFile, type : type},
		  	dataType : 'text',
		  	type : 'POST',
		  		success : function(result) {
		  			alert(result);
		  			targetLi.remove();
		  		}
		  });
	  });

 

 

2-3. 게시물 등록과 첨부파일의 데이터베이스 처리

 

게시물의 등록 과정에서는 첨부파일의 상세조회는 의미가 없고, 단순히 새로운 첨부파일을 추가하거나 삭제해서 자신이 원하는 파일을 게시물 등록할 때 같이 포함하도록 한다. Ajax를 이용하는 경우 이미 어떠한 파일을 첨부로 처리할 것인지는 이미 완료된 상태이므로 남은 작업은 게시물이 등록될 때 첨부파일과 관련된 자료를 같이 전송하고, 이를 데이터베이스에 등록하는 것이다. 게시물의 등록은 <form> 태그를 통해서 이루어지므로, 이미 업로드된 첨부파일의 정보는 별도의 <input type = 'hidden'> 태그를 생성해서 처리한다.

 

이를 위해서는 첨부파일 정보를 태그로 생성할 때 첨부파일과 관련된 정보(data-uuid, data-filename, data-type)를 추가한다. 

 

 

 

<input type = 'hidden'>으로 처리된 첨부파일의 정보는 BoardVO로 수집된다. 

 

BoardVO 에는 attachList 라는 이름의 변수로 첨부파일의 정보를 수집하기 때문에 <input type = 'hidden'>의 name은 'attachList[인덱스번호]'와 같은 이름을 사용하도록 한다.

 

JSP 화면에서는 JavaScript를 이용해서 기존에 <form> 태그를 전송하는 부분을 아래와 같이 수정한다. 

 

	var formObj = $("form[role='form']");
	$("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);
			
			str += "<input type = 'hidden' name = 'attachList["+i+"].fileName' value = '" + jobj.data("filename")+"'>";
			str += "<input type = 'hidden' name = 'attachList["+i+"].uuid' value = '" + jobj.data("uuid") + "'>";
			str += "<input type = 'hidden' name = 'attachList["+i+"].uploadPath' value = '" + jobj.data("path") + "'>";
			str += "<input type = 'hidden' name = 'attachList["+i+"].fileType' value = '" + jobj.data("type") + "'>";
	
		});
		formObj.append(str).submit();
	});

 

브라우저에서 게시물 등록을 선택하면 이미 업로드된 항목들을 내부적으로 <input type = 'hidden'> 태그들로 만들어서 <form> 태그가 submit 될 때 같이 전송되도록 한다.

 

3. BoardController, BoardService의 처리

 

파라미터를 수집하는 BoardController는 별도의 처리 없이 전송되는 데이터가 제대로 수집되었는지를 먼저 확인한다. 

 

- BoardController.java 의 일부 

	@PostMapping("/register")
	public String register(BoardVO board, RedirectAttributes rttr) {
		log.info("===============================");
		log.info("register : " + board);
		
		if(board.getAttachList() != null) {
			board.getAttachList().forEach(attach -> log.info(attach));
		}
		
		log.info("===============================");
        
        // 확인한 후에 반드시 주석 해제해주어야 함!!
        // service.register(board);
        // rttr.addFlashAttribute("result", board.getBno());
		return "redirect:/board/list";
	}

 

BoardController의 register()는 BoardService를 호출하기 전에 log를 이용해서 확인하는 작업을 먼저 진행한다. 브라우저에서 첨부파일을 추가하고 게시물을 등록하면 서버에서는 아래와 같은 로그들이 출력되는 것을 볼 수 있다. 이때 첨부파일이 이미지인지 여부에 따라서 fileType 등이 제대로 처리되는지 확인한다. 

 

(그런데 내꺼 지금 한글이 깨졌다 무슨일이지... ) -> web.xml에 필터를 추가하면 된다!

 

3-1. BoardServiceImpl 처리

 

BoardMapper와 BoardAttachMapper는 이미 작성해 두었기 때문에 남은 작업은 BoardServiceImpl에서 두 개의 Mapper 인터페이스 타입을 주입하고, 이를 호출하는 일이다. 2개의 Mapper를 주입받아야 하기 때문에 자동주입 대신에 Setter 메서드를 이용하도록 수정한다. 

 

- BoardServiceImpl 클래스의 일부

@Log4j
@Service
@AllArgsConstructor
public class BoardServiceImpl implements BoardService{

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

 

게시물의 등록 작업은 tbl_board 테이블과 tbl_attach 테이블 양쪽 모두 insert가 진행되어야 하기 때문에 트랜잭션 처리가 필요하다. 일반적인 경우라면 오라클의 시퀀스를 이용해서 nextval과 currval을 이용해서 처리하겠지만, 예제는 이미 MyBatis의 selectkey를 이용했기 때문에 별도의 currval을 매번 호출할 필요는 없다. 

 

	@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);
		});
	}

 

 

BoardServiceImpl의 register()는 트랜잭션 하에서 tbl_board에 먼저 게시물을 등록하고, 각 첨부파일은 생성된 게시물 번호를 세팅한 후 tbl_attach 테이블에 데이터를 추가한다. MyBatis 쪽에 문제가 없다면 데이터베이스의 tbl_attach 테이블에 첨부파일이 여러 개 등록되었을 때 아래와 같은 모습으로 출력되는 것을 볼 수 있다.