관리 메뉴

Just Do it

Part 6) 파일 업로드 처리 (Ch 25) 본문

* 카테고리의 글은 <코드로 배우는 스프링  프로젝트에서  파트별로 진행하는 프로젝트의 흐름을 보기 위해  장의 내용을 간단히 요약한 것이다.

출처코드로 배우는 스프링  프로젝트 개정판구멍가게 코딩단남가람북스 

 

Ch 25. 프로젝트의 첨부파일 - 등록

- 첨부파일은 게시물의 등록/조회/수정/삭제 화면에서 처리할 필요가 있으므로 각 단계마다 나누어서 개발을 진행한다.

 

25.1 첨부파일 정보를 위한 준비 

- 첨부파일이 게시물과 합쳐지면 가장 먼저 진행해야 하는 일은 게시물과 첨부파일의 관계를 저장하는 테이블의 설계가 우선이다.

- 게시물의 첨부파일은 각자 고유한 UUID를 가지고 있기 때문에 별도의 PK를 지정할 필요는 없지만, 게시물을 등록할 때 첨부파일 테이블 역시 같이 insert 작업이 되어야 하므로 트랜잭션 처리가 필요하다 .

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

CREATE TABLE TBL_ATTACH (
    UUID VARCHAR2(100) NOT NULL, -- UUID 값 
    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);

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

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

25.1.1 첨부파일 처리를 위한 Mapper 처리 

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

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

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

 

25.2 등록을 위한 화면 처리 

- 첨부파일 자체의 처리는 Ajax를 통해서 이루어지므로, 게시물의 등록 시점에는 현재 서버에 업로드된 파일들에 정보를 등록하려는 게시물의 정보와 같이 전송해서 처리한다.

- 이 작업은 게시물의 등록 버튼을 클릭했을 때 현재 서버에 업로드된 파일의 정보를 <input type='hidden'> 으로 만들어서 한 번에 전송하는 방식을 사용한다.

 

- 게시물의 등록을 담당하는 register.jsp 파일에서 첨부파일을 추가할 수 있도록 수정하는 작업부터 시작한다. 기존 게시물의 제목이나 내용을 입력하는 부분 아래쪽에 첨부파일 등록을 위한 새로운 <div>를 추가한다. 

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

 

 

25.2.1 JavaScript 처리 

- 복잡한 부분은 파일을 선택하거나 'Submit Button'을 클릭했을 때의 자바스크립트 처리이다.

- 가장 먼저 'Submit Button'을 클릭했을 때 첨부파일 관련된 처리를 할 수 있도록 기본 동작을 막는 작업부터 시작한다. 

- 파일의 업로드는 별도의 업로드 버튼을 두지 않고, <input type='file'>의 내용이 변경되는 것을 감지해서 처리하도록 한다.

- $(document).ready() 내에 파일 업로드 시 필요한 코드를 추가한다.

- 첨부된 파일 처리는 기존과 동일하지만 아직은 섬네일이나 파일 아이콘을 보여주는 부분은 처리하지 않는다. 

- 브라우저의 콘솔창을 이용해서 업로드가 정상적으로 처리되는지만 확인한다. 

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

<script type="text/javascript">

$(document).ready(function(e){
	var formObj = $("form[role='form']");
	$("button[type='submit']").on("click", function(e){
		e.preventDefault();
		console.log("submit clicked");
	});
	
	// 파일 확장자 exe.sh.zip.alz 업로드 금지 
	var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
	
	// 파일 당 최대 용량 5MB로 지정 
	var maxSize = 5242880;
	
	// 파일 확장자와 용량 확인  
	function checkExtension(fileName, fileSize){
		if (fileSize > maxSize) {
			alert("파일 당 용량은 최대 5MB까지만 가능합니다.")
			return false;
		}
		if (regex.test(fileName)) {
			alert("해당 종류의 파일은 첨부할 수 없습니다.(.exe, .sh, .zip, .alz 첨부 불가)")
			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]);
		}
		
	// 업로드된 결과를 화면에 섬네일 등을 만들어서 처리한다.
	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 + "/s_" + obj.uuid + "_" + obj.fileName );
				
				str += "<li><div>";
				str += "<span>" + obj.fileName + "</span>";
				str += "<button type='button' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button>";
				str += "<img src='/display?fileName=" + fileCallPath + "'>";
				str += "</div></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' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
				str += "<img src='/resources/img/attach.png'></a>";
				str += "</div></li>";
			}
		});
		
		uploadUL.append(str);
		
	}
		
		//파일 업로드 작업 비동기 ajax 처리 
		$.ajax({
			url:'/uploadAjaxAction',
			processData: false,
			contentType: false, 
			type: 'POST',
			data: formData, // 서버에 보내는 데이터 
			dataType: 'json', // 서버에서 받아올 데이터 
			success: function(result){
				console.log(result);
				showUploadResult(result); // 파일 업로드 결과를 처리해주는 함수 
			}
			
		});
	});
});

</script>

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

 

25.2.2 첨부파일의 변경 처리 

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

- 삭제를 위해서는 업로드된 파일의 경로와 UUID가 포함된 파일 이름이 필요하므로 앞서 작성된 showUploadResult 함수 부분을 수정해주면 된다. showUploadResult() 함수 내 <button> 태그에 'data-file'과 'data-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></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></li>";
			}
...

- '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',
			sucess: function(result){
				targetLi.remove();
			}
		});

- 브라우저에서 첨부파일을 삭제하면 업로드된 파일도 같이 삭제되는 것을 확인할 수 있다.

 

 

25.2.3 게시물 등록과 첨부파일의 데이터베이스 처리 

- 게시물의 등록 과정에서는 첨부파일의 상세 조회는 의미가 없고, 단순히 새로운 첨부파일을 추가하거나 삭제해서 자신이 원하는 파일을 게시물 등록할 때 같이 포함하도록 한다. 

- Ajax를 이용하는 경우 이미 어떠한 파일을 첨부로 처리할 것인지는 이미 완료된 상태이므로 남은 작업은 게시물이 등록될 때 첨부파일과 관련된 자료를 같이 전송하고, 이를 데이터베이스에 등록 하는 것이다.

- 게시물의 등록은 <form> 태그를 통해서 이루어지므로, 이미 업로드된 첨부파일의 정보는 별도의 <input type='hidden'> 태그를 생성해서 처리한다. 

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

...
$(uploadResultArr).each(function(i, obj){
			// 이미지 파일인 경우 
			if(obj.image){
				
				var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName );
				
				str += "<li data-path='"+ obj.uploadPath + "' data-uuid='" + obj.uuid + "' data-filename='" + obj.fileName+ "' data-type='"+ obj.image+ "'><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></li>";
				
			// 일반 파일인 경우 
			}else {
				
				var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName );
				var fileLink = fileCallPath.replace(new RegExp(/\\/g),"/");
				
				str += "<li data-path='"+ obj.uploadPath + "' data-uuid='" + obj.uuid + "' data-filename='" + obj.fileName+ "' data-type='"+ obj.image+ "'><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></li>";
			}
		});
...

 

 

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

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

- register.jsp 화면에서는 자바스크립트를 이용해서 기존 <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();
	});

 

25.3 BoardController, BoardService의 처리 

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

- BoardController의 register()는 BoardService를 호출하기 전에 log를 이용해서 확인하는 작업을 먼저 진행한다.

- 브라우저에서 첨부파일을 추가하고 게시물을 등록하면 서버에서는 아래와 같은 로그들이 출력되는 것을 볼 수 있다.

- 이때 첨부파일이 이미지인지 여부에 따라서 fileType 등이 제대로 처리되는지 확인한다. 

25.3.1 BoardServiceImpl 처리 

- BoardMapper와 BoardAttachMapper는 이미 작성해 두었기 때문에 남은 작업은 BoardServiceImpl에서 두 개의 Mapper 인터페이스 타입을 주입하고, 이를 호출하는 일이다.

- 2개의 Mapper를 주입받아야 하기 때문에 자동주입 대신에 Setter 메서드를 이용하도록 BoardServiceImpl을 수정한다. 

*수정 전(@AllArgsConstructor 를 이용한 자동주입) 

*수정 후 (@Setter, @Autowired 이용)

- 게시물의 등록 작업은 tbl_board 테이블과 tbl_attach 테이블 양쪽 모두 insert가 진행되어야 하기 때문에 트랜잭션 처리가 필요하다.

- 일반적인 경우라면 오라클의 시퀀스(sequence)를 이용해서 nextval과 currval을 이용해서 처리하겠지만, 예제는 이미 MyBatis의 selectkey를 이용했기 때문에 별도의 currval을 매번 호출할 필요는 없다. 

- BoardServiceImply 내 register() 메서드에 @Transactional 어노테이션을 추가하고 아래와 같이 메서드를 수정해준다.

*수정 전) 

*수정 후)

- BoardServiceImpl의 register()는 트랜잭션 하에서 tbl_board에 먼저 게시물을 등록하고, 각 첨부파일은 생성된 게시물 번호를 셋팅한 후 tbl_attach 테이블에 데이터를 추가한다.

- MyBatis 쪽에 문제가 없다면 데이터베이스의 tbl_attach 테이블에 첨부파일이 여러 개 등록되었을 때 아래와 같은 모습으로 출력되는 것을 볼 수 있다.