개발/[Spring] 블로그 만들기

[코드로 배우는 스프링 웹 프로젝트] 25강. 프로젝트의 첨부파일 - 등록

ee2ee2 2022. 10. 1. 23:58
728x90
반응형

해당 프로젝트는 코드로 배우는 스프링 웹 프로젝트(개정판)을 기반으로 진행됩니다.


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

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

첨부파일 테이블 명 : tbl_attach

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

 

SQL을 처리하기 위해서는 파일 정보를 처리하기 위해 파라미터를 여러 개 사용해야 하므로, org.zerock.domain 패키지에 BoardAttachVO 클래스를 설계해보자.

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>를 추가한다. (첨부파일은 게시글 하나에 여러 개 일 수 있으므로, List로 생성한다.)

BoardVO.java

package org.zerock.domain;

import java.util.Date;
import java.util.List;

import lombok.Data;

@Data
public class BoardVO {
	private Long bno;
	private String title;
	private String content;
	private String writer;
	private Date cdate;
	private Date udate;
	
	private int replyCnt;
	
	//게시글 등록시 한 번에 첨부파일도 처리하기 위함.
	private List<BoardAttachVO> attachList;
}

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

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

BoardAttachMapper.java 인터페이스

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.xml

첨부파일 수정은 없기에 insert/delete/findBybno(게시글에 등록된 첨부파일을 찾기 위함)만 구현한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper 
PUBLIC "-//mtbatis.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>

25.2 등록을 위한 화면 처리

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

register.jsp

기존 게시물의 제목이나 내용을 입력하는 부분 아래 쪽에 새로운 첨부파일 <div>를 추가

//맨 위 상단에 css 적용 
//<style><%@ include file="/WEB-INF/views/includes/uploadAjax.css" %></style>

//..생략
                        <!-- /.end panel-body -->
                    </div>
                    <!-- /.end panel -->
                </div>
                <!-- /.col-lg-12 -->
            </div>
            <!-- /.row -->
            
            <!-- 첨부파일을 추가 전달하는 부분 -->
            <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="form-group uploadDiv">
		            			<input type="file" name='uploadFile' multiple/>
		            		</div>
	            		
		            		<div class='uploadResult'>
		            			<ul>
		            			
		            			</ul>
		            		</div>
	            		
	            		</div>
	            		<!-- end panel-body -->
	            	</div>
	            	<!-- end panel-body -->
	            </div>
	            <!-- end panel -->
            </div>
            <!-- /.row -->

실행 결과


25.2.1 JavaScript의 처리

register.jsp의 일부

<script>
	<!-- Submit Button을 클릭하였을 때, 첨부파일 관련된 처리를 할 수 있도록 기본 동작을 막는 작업 -->
	$(document).ready(function(e){
		
		var formObj = $("form[role='form']");
		
		$("button[type='submit']").on("click", function(2){
			e.preventDefault();
			
			console.log("submit clicked");
		});
		
	});
	
</script>

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

register.jsp의 일부

<script>
	<!-- Submit Button을 클릭하였을 때, 첨부파일 관련된 처리를 할 수 있도록 기본 동작을 막는 작업 -->
	$(document).ready(function(e){
		
		var formObj = $("form[role='form']");
		
/* 		$("button[type='submit']").on("click", function(2){
			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++){
				
				//파일 사이즈 초과하거나, 업로드할 수 없는 확장자라면 return
				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);
						//showUploadResult(result); //업로드 결과 처리 함수
					}
			}); //$.ajax
		});
		
	});
	
</script>

실행 결과

아직 섬네일이나 파일 아이콘을 보여주는 부분은 처리되지 않았다. 브라우저의 콘솔창을 이용해서 업로드가 정상적으로 처리되는 지만을 확인한다.

<파일 정상 업로드 확인>

 

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

register.jsp의 일부

showUploadResult() 는 Ajax를 호출 후에 업로드된 결과를 처리하는 함수이므로, 바로 위의 코드에서 $.ajax() 호출 부분의 주석 처리한 부분을 해제한다.

		//업로드된 결과를 화면에 섬네일 등을 만들어서 처리하는 부분
		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></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'>";
					str+= "</div></li>";
				}
			});
			
			console.log(str);
			uploadUL.append(str);
		}

<span> 태그 : 줄단위의 영역으로 잡히며, 태그영역 뒤에 줄바꿈을 포함하지 않는다.

실행 결과

반응형

25.2.2 첨부파일의 변경 처리

'x' 버튼을 누르면 업로드된 파일의 삭제 처리를 구현한다.

register.jsp의 일부

		$(".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();
					}
			}); //$.ajax
		});

실행결과

<bbb.txt 삭제>


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

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

register.jsp

첨부파일 정보를 태그로 생성할 때 관련된 정보를 추가하도록 하자. (data-uuid, data-filename, data-type)

		//업로드된 결과를 화면에 섬네일 등을 만들어서 처리하는 부분
		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 data-path='" + obj.uploadPath + "'";
					str += " data-uuid='" + obj.uuid + "' data-filename='" + obj.fileName + "' data-type='" + obj.image + "'";
					str += " ><div>";
					str += "<span>" + obj.fileName + "</span>";
					str += "<button type='button' data-file=\'" + fileCallPath + "\' ";
					str += "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 data-path='" + obj.uploadPath + "'";
					str += " data-uuid='" + obj.uuid + "' data-filename='" + obj.fileName + "' data-type='" + obj.image + "'";
					str += " ><div>";
					str += "<span>" + obj.fileName + "</span>";
					str += "<button type='button' data-file=\'" + fileCallPath + "\' ";
					str += "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>";
				}
			});
			
			console.log(str);
			uploadUL.append(str);
		}

실행 결과

업로드된 정보는 JSON으로 처리된다.

 

register.jsp

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

	$(document).ready(function(e){
		
		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);
				
				//BoardVO에서 attachList라는 이름의 변수로 첨부파일의 정보를 수집하고 있음. 따라서, <input type='hidden'>의 name은 'attachList[idx]'와 같은 이름을 사용하도록 함
				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.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"; //'redirect:' : 스프링 MVC가 내부적으로 response.serdRedirect()를 처리함
	}

실행결과


25.3.1 BoardServiceImpl 처리

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

BoardServiceImpl .java 클래스 일부

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

tbl_board에 먼저 게시글을 등록하고, 각 첨부파일은 생성된 게시물 번호를 세팅한 후 tbl_attach 테이블에 데이터를 추가한다.

@Log4j
@Service
public class BoardServiceImpl implements BoardService{

	@Setter(onMethod_ = @Autowired)
	private BoardMapper mapper;
	
	@Setter(onMethod_= @Autowired)
	private BoardAttachMapper attachMapper;
	
	@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);
		});
	}

실행 결과


다음에는 게시물을 조회할 때, 등록된 첨부파일도 같이 조회를 할 수 있도록 구현해 볼 것이다!