ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링(Spring) 개발 - (15) 파일 업로드 & 다운로드 (3/3)
    Spring 2015.08.03 17:42

    그간 일이 바빠서 글을 참 오랜만에 쓰게 되네요.


    이번글에서 첨부파일에 관련된 것을 마무리 합니다. 


    ------------------------------------------------------------------------------------


    1. 첨부파일 다중 업로드 

    지난글에서 단일 첨부파일 업로드를 했었는데, 그것을 수정해서 여러개의 첨부파일을 등록하도록 수정하자.


    1. JSP

    먼저 boardWrite.jsp를 다음과 같이 수정하자.

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html lang="ko">
    <head>
    <%@ include file="/WEB-INF/include/include-header.jspf" %>
    </head>
    <body>
    	<form id="frm" name="frm" enctype="multipart/form-data">
    		<table class="board_view">
    			<colgroup>
    				<col width="15%">
    				<col width="*"/>
    			</colgroup>
    			<caption>게시글 작성</caption>
    			<tbody>
    				<tr>
    					<th scope="row">제목</th>
    					<td><input type="text" id="TITLE" name="TITLE" class="wdp_90"></input></td>
    				</tr>
    				<tr>
    					<td colspan="2" class="view_text">
    						<textarea rows="20" cols="100" title="내용" id="CONTENTS" name="CONTENTS"></textarea>
    					</td>
    				</tr>
    			</tbody>
    		</table>
    		<div id="fileDiv">
    			<p>
    				<input type="file" id="file" name="file_0">
    				<a href="#this" class="btn" id="delete" name="delete">삭제</a>
    			</p>
    		</div>
    		
    		<br/><br/>
    		<a href="#this" class="btn" id="addFile">파일 추가</a>
    		<a href="#this" class="btn" id="write">작성하기</a>
    		<a href="#this" class="btn" id="list">목록으로</a>
    	</form>
    	
    	<%@ include file="/WEB-INF/include/include-body.jspf" %>
    	<script type="text/javascript">
    		var gfv_count = 1;
    	
    		$(document).ready(function(){
    			$("#list").on("click", function(e){ //목록으로 버튼
    				e.preventDefault();
    				fn_openBoardList();
    			});
    			
    			$("#write").on("click", function(e){ //작성하기 버튼
    				e.preventDefault();
    				fn_insertBoard();
    			});
    			
    			$("#addFile").on("click", function(e){ //파일 추가 버튼
    				e.preventDefault();
    				fn_addFile();
    			});
    			
    			$("a[name='delete']").on("click", function(e){ //삭제 버튼
    				e.preventDefault();
    				fn_deleteFile($(this));
    			});
    		});
    		
    		function fn_openBoardList(){
    			var comSubmit = new ComSubmit();
    			comSubmit.setUrl("<c:url value='/sample/openBoardList.do' />");
    			comSubmit.submit();
    		}
    		
    		function fn_insertBoard(){
    			var comSubmit = new ComSubmit("frm");
    			comSubmit.setUrl("<c:url value='/sample/insertBoard.do' />");
    			comSubmit.submit();
    		}
    		
    		function fn_addFile(){
    			var str = "<p><input type='file' name='file_"+(gfv_count++)+"'><a href='#this' class='btn' name='delete'>삭제</a></p>";
    			$("#fileDiv").append(str);
    			$("a[name='delete']").on("click", function(e){ //삭제 버튼
    				e.preventDefault();
    				fn_deleteFile($(this));
    			});
    		}
    		
    		function fn_deleteFile(obj){
    			obj.parent().remove();
    		}
    	</script>
    </body>
    </html>
    

    파일 추가 버튼과 삭제버튼이 추가 되었다. 

    먼저 파일 추가버튼을 누르면 fileDiv라는 파일 영역의 마지막에 새로운 파일 태그 및 삭제버튼을 추가하도록 하였다. 

    그 후 추가된 삭제버튼에도 삭제 기능을 위한 클릭이벤트를 바인딩 하였다. 


    여기서 살펴볼 것은 <input type='file'> 태그의 name이 동일할 경우, 서버에는 단 하나의 파일만 전송되는 문제가 발생한다. 따라서 gfv_count 라는 전역변수를 선언하고, 태그가 추가될때마다 그 값을 1씩 증가시켜서 name값이 계속 바뀌도록 하였다. 


    사실 여기서는 핵심적인 부분만 설명하기 위해서 간략히 작성을 하였는데, 실제로는 좀 더 복잡한 처리가 여러가지 필요하다. 

    예를 들어, 파일의 크기나 유효성 검사도 하지 않았으며, 추가할 수 있는 파일의 개수도 제한하지 않았다. 

    또한 파일의 전송에 따라서 파일의 순서가 바뀔수도 있기 때문에, 첨부파일의 순서를 지정하는 작업도 필요하다. 

    그렇지만 그런부분은 사람마다 구현하는 방식이 다르고, 또 여기서 그런것들을 모두 설명하기에는 양이 적지않아서 기본적인 내용만 보고 넘어가도록 하겠다.


    그 다음으로 삭제버튼이다.

    삭제버튼을 누르면 해당 버튼이 위치한 <p>태그 자체를 삭제하도록 구성하였다. 


    그럼 실행을 시켜보자. 서버 및 쿼리는 지난글에서 다중 파일 업로드를 고려해서 작성을 했었기때문에, 다시 수정할 건 없다. 

    최초로 실행시키면 다음과 같은 화면을 볼 수 있다.


    여기서 파일 추가를 클릭하면 파일을 입력할 수 있는 새로운 태그가 생성된다.


    여러개의 파일을 추가하고 삭제해보고 게시글을 등록하고 확인해보자.


    3개의 파일 태그가 존재하고 1.PNG, 2.PNG라는 2개의 파일을 첨부하였다. 

    이 상태로 작성하기를 누르면 게시글이 저장되고 목록으로 이동이 될 것이다. 바로 상세화면에서 파일이 제대로 올라갔는지 확인을 해 보자. 


    위와 같이 두개의 파일이 정상적으로 등록이 된 것을 확인할 수 있다. 

    (기존의 boardDetail.jsp 에서 첨부파일의 목록을 보여주던 <c:forEach>문 사이에 <p>태그를 추가하였다.)


    다음으로는 이클립스의 로그를 확인해보자.

    이클립스의 로그가 좀 잘렸지만, 필요한 내용은 다 확인할 수 있다. 

    먼저 게시글을 등록한 다음, 첨부파일의 정보를 순차적으로 저장한 것을 확인할 수 있다. 


    2. 첨부파일 수정

    이번글의 가장 핵심적인 부분이다. 다음에 설명하는 부분에 대해서 한번 고민을 하는 시간을 가져보자.

    지난 두개의 글에서 첨부파일의 등록 및 다운로드에 대해서 이야기를 했었고, 이번에는 첨부파일의 수정에 대해서 이야기를 할 차례이다. 


    프로그램을 작성하기에 앞서 첨부파일의 수정을 어떻게 처리할 것인지 고민을 해야한다. 

    게시글을 수정할 때 해당되는 첨부파일의 수정은 등록과 다르게 좀 복잡한 프로세스를 가진다. 

    다음의 경우를 생각해보자.

    1) 게시글의 내용만 수정을 하고, 첨부파일은 수정하지 않는다.

    2) 첨부파일을 수정할 때 기존에 등록한 파일을 변경한다. 

    3) 기존에 등록한 파일은 놔두고, 새로운 파일을 추가한다. 

    4) 기존에 등록한 파일을 모두 삭제하고, 새로운 파일을 등록한다.

    5) 기존에 등록한 파일의 일부를 삭제하고, 새로운 파일을 등록한다.


    간단히 생각나는 경우의 수만 생각해도 이렇게 5개가 나왔다. 

    첨부파일의 수정은 여러가지 경우의 수가 있기 때문에, 그 처리에 있어서 고민을 해야한다.


    여기서는 필자가 사용하는 수정 프로세스를 먼저 이야기 하도록 하겠다. 

    필자는 게시글 및 해당 첨부파일을 수정할 때, 먼저 해당 게시글의 첨부파일 목록을 모두 삭제처리한다. 

    여기서 삭제처리의 의미는 실제 파일을 삭제하는것이 아니라, DEL_GB의 값을 모두 'Y"로 변경하는 것을 의미한다. 


    그 다음으로 FileUtils클래스에서 파일정보를 list로 변경할 때, 기존에 첨부가 되어있던 파일의 정보와 신규로 입력된 파일 정보를 구분한다.

    그 후, 기존에 첨부가 되어있던 파일은 DEL_GB값을 다시 N으로 변경(update)하고, 신규 추가된 파일 정보는 입력(insert)한다.


    이렇게 하면 DB에는 기존에 등록했던 파일 정보까지 모두 남아있지만, 삭제된 파일과 현재 사용중인 파일을 DEL_GB값을 이용해서 구분할 수 있다.

    단, 실제파일을 삭제하는것은 아니기 때문에 서버에는 모든 파일이 남아있게 된다. 


    이 방식은 각각 장,단점이 존재한다. 

    만약 실제 파일을 삭제하는것은 HDD를 사용하기 때문에, 서버의 부담이 커지고 당연히 속도도 저하될 수 밖에 없다. 그렇지만 서버에는 꼭 필요한 파일만 남아있기 때문에 서버의 용량관리에는 좀 더 유리하다.

    반대로 필자의 경우, 파일을 삭제하지 않기 때문에 서버속도는 좀 더 빠르지만, 삭제처리된 파일을 그대로 가지고 있기 때문에 용량은 좀 더 많이 차지할수밖에 없다. 

    따라서, 개발하는 시스템의 성격 및 개발 방식, 하드웨어의 상황에 따라서 유연하게 구성해야함을 잊지말자.

    아직까지는 이게 무슨 이야기인지 바로 와닿지 않을것이라고 생각된다. 


    이제, 위에서 이야기한 내용을 실제로 구현을 해 보자.


    1. Controller

    지난글에서 sampleService.selectBoardDetail 의 리턴값을 변경하였었다. 

    기존에는 selectBoardDetail을 호출하면 게시글의 내용만 반환하였는데(http://addio3305.tistory.com/79 3. 상세페이지 > 2.Java > SampleServiceImpl 참조), 첨부파일을 추가하면서 게시글의 내용과 첨부파일 목록 2가지를 반환하도록 변경하였다. (http://addio3305.tistory.com/84 의 1.첨부파일 보여주기 > 2.java > 2) SampleServiceImpl 참조)

    @RequestMapping(value="/sample/openBoardUpdate.do")
    public ModelAndView openBoardUpdate(CommandMap commandMap) throws Exception{
    	ModelAndView mv = new ModelAndView("/sample/boardUpdate");
    	
    	Map<String,Object> map = sampleService.selectBoardDetail(commandMap.getMap());
    	mv.addObject("map", map.get("map"));
    	mv.addObject("list", map.get("list"));
    	
    	return mv;
    }
    


    2. JSP

    boardUpdate.jsp를 다음과 같이 수정하자.

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html lang="ko">
    <head>
    <%@ include file="/WEB-INF/include/include-header.jspf" %>
    </head>
    <body>
    	<form id="frm" name="frm" enctype="multipart/form-data">
    		<table class="board_view">
    			<colgroup>
    				<col width="15%"/>
    				<col width="35%"/>
    				<col width="15%"/>
    				<col width="35%"/>
    			</colgroup>
    			<caption>게시글 상세</caption>
    			<tbody>
    				<tr>
    					<th scope="row">글 번호</th>
    					<td>
    						${map.IDX }
    						<input type="hidden" id="IDX" name="IDX" value="${map.IDX }">
    					</td>
    					<th scope="row">조회수</th>
    					<td>${map.HIT_CNT }</td>
    				</tr>
    				<tr>
    					<th scope="row">작성자</th>
    					<td>${map.CREA_ID }</td>
    					<th scope="row">작성시간</th>
    					<td>${map.CREA_DTM }</td>
    				</tr>
    				<tr>
    					<th scope="row">제목</th>
    					<td colspan="3">
    						<input type="text" id="TITLE" name="TITLE" class="wdp_90" value="${map.TITLE }"/>
    					</td>
    				</tr>
    				<tr>
    					<td colspan="4" class="view_text">
    						<textarea rows="20" cols="100" title="내용" id="CONTENTS" name="CONTENTS">${map.CONTENTS }</textarea>
    					</td>
    				</tr>
    				<tr>
    					<th scope="row">첨부파일</th>
    					<td colspan="3">
    						<div id="fileDiv">				
    							<c:forEach var="row" items="${list }" varStatus="var">
    								<p>
    									<input type="hidden" id="IDX" name="IDX_${var.index }" value="${row.IDX }">
    									<a href="#this" id="name_${var.index }" name="name_${var.index }">${row.ORIGINAL_FILE_NAME }</a>
    									<input type="file" id="file_${var.index }" name="file_${var.index }"> 
    									(${row.FILE_SIZE }kb)
    									<a href="#this" class="btn" id="delete_${var.index }" name="delete_${var.index }">삭제</a>
    								</p>
    							</c:forEach>
    						</div>
    					</td>
    				</tr>
    			</tbody>
    		</table>
    	</form>
    	
    	<a href="#this" class="btn" id="addFile">파일 추가</a>
    	<a href="#this" class="btn" id="list">목록으로</a>
    	<a href="#this" class="btn" id="update">저장하기</a>
    	<a href="#this" class="btn" id="delete">삭제하기</a>
    	
    	<%@ include file="/WEB-INF/include/include-body.jspf" %>
    	<script type="text/javascript">
    		var gfv_count = '${fn:length(list)+1}';
    		$(document).ready(function(){
    			$("#list").on("click", function(e){ //목록으로 버튼
    				e.preventDefault();
    				fn_openBoardList();
    			});
    			
    			$("#update").on("click", function(e){ //저장하기 버튼
    				e.preventDefault();
    				fn_updateBoard();
    			});
    			
    			$("#delete").on("click", function(e){ //삭제하기 버튼
    				e.preventDefault();
    				fn_deleteBoard();
    			});
    			
    			$("#addFile").on("click", function(e){ //파일 추가 버튼
    				e.preventDefault();
    				fn_addFile();
    			});
    			
    			$("a[name^='delete']").on("click", function(e){ //삭제 버튼
    				e.preventDefault();
    				fn_deleteFile($(this));
    			});
    		});
    		
    		function fn_openBoardList(){
    			var comSubmit = new ComSubmit();
    			comSubmit.setUrl("<c:url value='/sample/openBoardList.do' />");
    			comSubmit.submit();
    		}
    		
    		function fn_updateBoard(){
    			var comSubmit = new ComSubmit("frm");
    			comSubmit.setUrl("<c:url value='/sample/updateBoard.do' />");
    			comSubmit.submit();
    		}
    		
    		function fn_deleteBoard(){
    			var comSubmit = new ComSubmit();
    			comSubmit.setUrl("<c:url value='/sample/deleteBoard.do' />");
    			comSubmit.addParam("IDX", $("#IDX").val());
    			comSubmit.submit();
    			
    		}
    		
    		function fn_addFile(){
    			var str = "<p>" +
    					"<input type='file' id='file_"+(gfv_count)+"' name='file_"+(gfv_count)+"'>"+
    					"<a href='#this' class='btn' id='delete_"+(gfv_count)+"' name='delete_"+(gfv_count)+"'>삭제</a>" +
    				"</p>";
    			$("#fileDiv").append(str);
    			$("#delete_"+(gfv_count++)).on("click", function(e){ //삭제 버튼
    				e.preventDefault();
    				fn_deleteFile($(this));
    			});
    		}
    		
    		function fn_deleteFile(obj){
    			obj.parent().remove();
    		}
    	</script>
    </body>
    </html>
    

    먼저 실행된 화면을 확인한 후에, 하나씩 확인을 하자.


    게시글 상세에서 수정하기 버튼을 눌러서 게시글 수정 화면을 보면 다음과 같다.


    기존에 저장이 된 파일명과 해당 파일을 수정할 수 있는 파일 선택 버튼, 그리고 삭제버튼으로 구성하였다. 

    파일추가 버튼은 boardWrite.jsp에서 만든 기능과 동일하게 첨부파일을 하나 추가하도록 구성하였다. 


    여기서 하나 자세히 봐야하는것은 첨부파일 목록부분에서 만든 <input type="hidden" id="IDX" name="IDX_${var.index}"> 태그이다. 이 태그의 name 속성이 IDX_숫자 로 구성이 되어있는것을 기억해야 한다. 

    위에서 파일수정 프로세스를 이야기할 때 "FileUtils클래스에서 파일정보를 list로 변경할 때, 기존에 첨부가 되어있던 파일의 정보와 신규로 입력된 파일 정보를 구분한다." 라는 이야기를 했었다. 

    기존에 저장된 파일에서는 해당파일번호인 IDX 값이 존재하는데, 이를 이용해서 신규파일정보와 아닌것을 구분하려고 한다.

    그 외에는 앞에서 이야기한 내용과 다른점이 없기 때문에 넘어가도록 하겠다.


    3. Java

    1) Controller

    SampleController.java의 updateBoard.do를 다음과 같이 변경한다.

    @RequestMapping(value="/sample/updateBoard.do")
    public ModelAndView updateBoard(CommandMap commandMap, HttpServletRequest request) throws Exception{
    	ModelAndView mv = new ModelAndView("redirect:/sample/openBoardDetail.do");
    	
    	sampleService.updateBoard(commandMap.getMap(), request);
    	
    	mv.addObject("IDX", commandMap.get("IDX"));
    	return mv;
    }
    

    기존의 updateBoard.do에서 첨부파일 정보를 포함한 HttpServletRequest를 추가하였다.


    2) Service 및 ServiceImpl

    먼저, SampleService.java의 updateBoard()를 다음과 같이 변경한다.

    void updateBoard(Map<String, Object> map, HttpServletRequest request) throws Exception;
    

    다음으로 중요한것은 ServiceImpl 영역이다. 

    먼저 SampleServiceImpl.java의 updateBoard를 다음과 같이 변경하자.

    @Override
    public void updateBoard(Map<String, Object> map, HttpServletRequest request) throws Exception{
    	sampleDAO.updateBoard(map);
    	
    	sampleDAO.deleteFileList(map);
    	List<Map<String,Object>> list = fileUtils.parseUpdateFileInfo(map, request);
    	Map<String,Object> tempMap = null;
    	for(int i=0, size=list.size(); i<size; i++){
    		tempMap = list.get(i);
    		if(tempMap.get("IS_NEW").equals("Y")){
    			sampleDAO.insertFile(tempMap);
    		}
    		else{
    			sampleDAO.updateFile(tempMap);
    		}
    	}
    }
    

    기존의 updateBoard에서는 sampleDAO.updateBoard(map) 한줄만 있었는데, 추가된 내용이 조금 있다. 

    일단 이렇게 작성을 하게되면 

    5번째 줄 dampleDAO.deleteFileList(map);

    6번째 줄 fileUtils.parseUpdateFileInfo(map, request);

    14번째 줄 sampleDAO.updateFile(tempMap); 

    에서 에러가 발생할 것이다. 


    일단 에러는 무시하고, 간단히 설명을 하도록 하겠다. 여기서는 위에서 첨부파일의 수정 프로세스를 염두해 두고서 생각하자.


    먼저, 게시글의 내용을 수정하는 sampleDAO.updateBoard는 기존과 같다. 

    그 다음으로 sampleDAO.deleteFileList(map)을 호출하는데, 이는 해당 게시글에 해당하는 첨부파일을 전부 삭제처리(DEL_GB = 'Y')를 하는 역할을 한다.

    이렇게해서 해당 게시글의 첨부파일은 전부 삭제가 된 상황이다.

    그 다음으로, fileUtils의 parseUpdateFileInfo 메서드를 이용해서 request에 담겨있는 파일 정보를 list로 변환한다. 이때, 기존에 저장된 파일 중에서 삭제되지 않은 파일정보도 함께 반환할 것이다. 

    그 다음으로는 파일을 하나씩 입력(insert) 또는 수정(update)을 할 차례인데, 이는 list에 담긴 파일정보중에서 IS_NEW라는 값을 이용해서 판별할 계획이다.

    IS_NEW라는 값이 "Y"인 경우는 신규 저장될 파일이라는 의미이고, "Y"가 아니면 기존에 저장되어 있던 파일이라는 의미이다.

    그래서 신규저장은 sampleDAO.insertFile을 이용하여 파일정보를 저장하고, 기존에 저장된 파일정보는 다시 삭제처리를 바꿔주기만 할 계획이다.


    그럼 이러한 기능을 수행할 FileUtils 클래스의 parseUpdateFileInfo 메서드를 살펴보자.

    public List<Map<String, Object>> parseUpdateFileInfo(Map<String, Object> map, HttpServletRequest request) throws Exception{
    	MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest)request;
    	Iterator<String> iterator = multipartHttpServletRequest.getFileNames();
    	
    	MultipartFile multipartFile = null;
    	String originalFileName = null;
    	String originalFileExtension = null;
    	String storedFileName = null;
    	
    	List<Map<String,Object>> list = new ArrayList<Map<String,Object>>();
    	Map<String, Object> listMap = null; 
    	
    	String boardIdx = (String)map.get("IDX");
    	String requestName = null;
    	String idx = null;
    	
    	
    	while(iterator.hasNext()){
    		multipartFile = multipartHttpServletRequest.getFile(iterator.next());
    		if(multipartFile.isEmpty() == false){
    			originalFileName = multipartFile.getOriginalFilename();
    			originalFileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
    			storedFileName = CommonUtils.getRandomString() + originalFileExtension;
    			
    			multipartFile.transferTo(new File(filePath + storedFileName));
    			
    			listMap = new HashMap<String,Object>();
    			listMap.put("IS_NEW", "Y");
    			listMap.put("BOARD_IDX", boardIdx);
    			listMap.put("ORIGINAL_FILE_NAME", originalFileName);
    			listMap.put("STORED_FILE_NAME", storedFileName);
    			listMap.put("FILE_SIZE", multipartFile.getSize());
    			list.add(listMap);
    		}
    		else{
    			requestName = multipartFile.getName();
    			idx = "IDX_"+requestName.substring(requestName.indexOf("_")+1);
    			if(map.containsKey(idx) == true && map.get(idx) != null){
    				listMap = new HashMap<String,Object>();
    				listMap.put("IS_NEW", "N");
    				listMap.put("FILE_IDX", map.get(idx));
    				list.add(listMap);
    			}
    		}
    	}
    	return list;
    }
    

    기존에 작성한 parseInsertFileInfo 메서드를 기반으로 약간 변경하였다.

    먼저 multipartFile이 비어있지 않은 경우, 즉 첨부파일이 있는 경우는 기존과 동일하다. 

    첨부파일이 있다는 것은 해당 파일이 변경됨을 뜻하고 이는 새롭게 저장을 해야한다. 따라서 기존에 parseInsertFileInfo에서 한것과 동일하게 파일을 저장하고 그걸 정보를 list에 추가하였다.

    여기서 다른점은 SampleServiceImpl 에서 사용할 "IS_NEW"라는 키로 "Y"라는 값을 저장하였다. 


    그 다음으로 봐야할게 else 문, 즉 multipartFile이 비어있는 경우(multipartFile.isEmpty() == true)이다.

    단순히 게시글을 작성할 경우에는 이는 무시하면 되는 부분이었다. 그렇지만 수정에서는 첨부파일이 없더라도 해당 파일은 저장이 될 수도 있고 아닐수도 있다는 것을 무시하면 안된다.

    게시글에서 파일을 수정하지 않을 경우, 해당 multipartFile은 비어있다. 그렇지만 그대로 놔두면 이미 파일은 지워져있기 때문에, 최종적으로는 파일이 없다고 나오게 된다. 

    따라서, 파일정보가 없는 경우에는 해당 파일정보가 기존에 저장이 되어있던 내용인지 아니면 단순히 빈 파일인지 구분해야한다. 

    그것을 구분하는게 37, 38번째 줄이다. 

    먼저 36번째 줄에서는 requestName = multipartFile.getName()이라고 되어있는데, 이는 html 태그에서 file 태그의 name 값을 가져오게 된다. 

    아까 jsp에서 파일 태그를 <input type="file" id="file_숫자" name="file_숫자"> 로 만들었던 것을 다시 한번 확인하자. 

    이 태그의 name값인 "file_숫자" 값을 가져오는 것이 multipartFile.getName() 메서드이다. 

    이 name에서 뒤에 있는 숫자를 가져오게 되면, map에서 IDX_숫자 값이 있는지 여부를 판별할 수 있다.


    기존에 저장이 되어있던 파일의 경우, <input type="hidden" id="IDX" name="IDX_숫자" value="파일번호"> 태그를 생성했던것을 기억해보자.

    그리고 신규로 생성이 된 파일의 경우, 위의 태그를 만들어주지 않았었다. 

    따라서, 위 태그의 값이 있을 경우가 기존에 저장된 파일임을 알 수 있다.

    그래서 36번째 줄은 "IDX_" 라는 키 값에 해당 태그의 네임에서 숫자를 가져와서 합쳐준다. 그렇게 되면 IDX_1, IDX_2 등의 값을 가지게 되는 것이다.

    그 다음 37번째 줄에서는 이제 화면에서 넘어온 값 중에서 IDX_숫자 키가 있는지를 확인하는 것이다.

    그래서 IDX_숫자 키가 있다면 그것은 기존에 저장이 되어있던 파일 정보임을 의미하는 "N" 이라는 값을 "IS_NEW"키로 저장하게 된다.


    4. DAO

    이제 SampleDAO에 미완성된 기능을 추가하면 된다.

    public void deleteFileList(Map<String, Object> map) throws Exception{
    	update("sample.deleteFileList", map);
    }
    
    public void updateFile(Map<String, Object> map) throws Exception{
    	update("sample.updateFile", map);
    }
    


    5. SQL

    이제 마지막으로 쿼리를 추가하면 된다.

    다음 두 개의 쿼리를 추가하자.

    <update id="deleteFileList" parameterType="hashmap">
    	<![CDATA[
    		UPDATE TB_FILE SET 
    			DEL_GB = 'Y' 
    		WHERE 
    			BOARD_IDX = #{IDX}	
    	]]>
    </update>
    
    <update id="updateFile" parameterType="hashmap">
    	<![CDATA[
    		UPDATE TB_FILE SET
    			DEL_GB = 'N'
    		WHERE
    			IDX = #{FILE_IDX}	
    	]]>
    </update>
    

    6. 실행 및 결과 확인

    이제 서버를 실행시키고 결과를 확인해 볼 차례이다. 

    몇가지 상황을 가정하고 정확히 동작하는지 확인해보자. 


    먼저 새로운 파일을 추가하는 경우이다. 

    이미지에서 볼수 있듯이 기존의 게시글에서 3.PNG라는 파일을 새롭게 추가하였다. 이제 저장하기를 눌러서 결과를 확인해보자.



    위에서 확인할 수 있듯이 3개의 파일이 정상적으로 저장된 것을 확인할 수 있다. 그럼 다음으로 이클립스의 로그를 살펴보자.



    게시글을 수정하기 위해서 여러개의 쿼리가 실행된 것을 확인할 수 있다.

    먼저 sample.updateBoard 쿼리가 수행되면서 게시글 정보가 변경된 것을 확인할 수 있다.

    그 다음으로 sample.deleteFileList 쿼리를 이용하여 해당 게시글의 모든 파일을 삭제처리 하였다. 

    그 다음으로 2번의 sample.updateFile 쿼리와 1번의 sample.insertFile 쿼리가 실행된 것을 확인할 수 있다. 

    기존에 저장되어 있던 1.PNG, 2.PNG 파일의 IDX는 각각 6,7번 이었다. (필자의 환경에서만 그렇다.) 

    따라서 두 개의 파일은 삭제여부만 다시 바꿔주면 되는 정보들이었고, DEL_GB = 'N' 으로 바뀌는것을 볼 수 있다.

    그 후, 새롭게 추가한 3.PNG는 기존에 없었던 파일이었기 때문에 신규로 정보가 저장된 것을 확인할 수 있다. 

    이렇게 해서 3개의 파일이 정상적으로 저장이 되었다. 


    그럼 다른 상황을 살펴보자.


    이번에는 2.PNG 파일을 4.PNG 파일로 변경하고, 빈 파일 태그를 하나 추가하였다. 

    이 상태로 저장하기를 누른 후 결과를 확인해보자.

    정상적으로 1.PNG, 3.PNG, 4.PNG 파일이 저장된 것을 볼 수 있다.

    쿼리 로그는 생략하도록 하겠다.


    마지막으로 파일의 변경 및 삭제가 동시에 일어나는 상황을 살펴보자.


    이번에는 1.PNG 파일을 삭제하고, 3.PNG 파일을 2.PNG 파일로 변경, 그리고 빈태그 하나와 5.PNG 파일을 첨부하였다. 

    이제 저장을 해보자.

    결과는 정상적으로 2.PNG, 4.PNG, 5.PNG 3개가 저장된 것을 볼 수 있다. 

    여기까지 하고 이클립스의 로그를 확인하는 것은 생략하고 이번에는 DB를 잠시 살펴보자.



    TB_FILE 테이블에서 BOARD_IDX가 8인 정보만 조회를 하였다. (위에서 실험을 했던 게시글 번호가 8 이다.)

    여기서 보면 그동안 수정을 했던 파일의 내역이 모두 남아있는 것을 확인할 수 있다. 그리고 현재 사용중인 파일은 DEL_GB의 값이 N인 파일만 사용중임을 알 수 있다. 


    이렇게 하나의 테이블에서 파일의 변경내역을 관리를 할 수도 있지만, 로그파일 또는 로그 DB등을 이용하여 별도로 관리할 수도 있다.

    그것은 해당 프로젝트마다 다르기 때문에, 프로젝트의 성격에 맞춰서 개발을 하면 된다.


    ------------------------------------------------------------------------------------


    이렇게 해서 파일 업로드, 다운로드가 끝이 났습니다.


    처음에는 2개의 글로 작성할 수 있을걸로 생각했는데, 생각보다 양이 많아져서 3부작으로 작성을 하게 되었네요.


    기초적인 부분은 어떻게 하고 이런 방식으로도 할수도 있으며, 왜 이런식으로 했는지를 가능한 전달을 하고 싶었는데, 그것이 잘 전달이 되었는지를 모르겠습니다.


    글에서도 계속 이야기를 했지만, 제가 하는 방식이 100% 옳은 방식이라고 이야기를 하지 않습니다. 


    프로젝트의 성격이나 요구사항, 하드웨어 상황에 따라서 개발하는 방법은 모두 다를겁니다. 


    단순히 제 글을 따라하는게 아니라, 그런 상황에 맞춘 개발을 할 수 있도록 작은 도움이 되었으면 합니다.


    first.zip





    댓글 98

    • 이전 댓글 더보기
    • 도레미 2016.04.01 09:54

      java.lang.ClassCastException: org.apache.catalina.connector.RequestFacade cannot be cast to org.springframework.web.multipart.MultipartHttpServletRequest 에러가 나는 경우에 resolver도 잘 설정이 되어있고 encType도 잘 설정이 되어있는데 혹시 내부 방화멱때문에 파일업로드가 안되는 경우도 있나요? 프로젝트나와서 짬나는시간에 하고있는데 추측이 안되네요..ㅎㅎ

    • vinniepaul 2016.10.05 17:48

      안녕하세요
      자주는 아니지만 짬날때마다 직접 해보고 있는데
      어느덧 여기까지 왔네요
      다름이 아니라
      분명 파일 업로드/다운로드 모든 기능 구현이 다 됐었는데 어제까지만해도..ㅠ
      갑자기 오늘 등록된 파일 3개중 1개를 삭제하고 2개만 저장하려는데
      1개 삭제가 되지 않는 겁니다..... 무엇 때문일까요;;;;
      deleteFileList 쿼리 실행할때 IDX 값을 가져오지 못하더라고요 null 로 찍히고
      updateFile 쿼리 실행할때 첨부파일 3개중 1개를 삭제하려했기 때문에 2개만 쿼리를 실행해야하는데
      3개 모두 업데이트가 되더라고요 문제가 뭘까요;;;; 장황하지만 도움 부탁드립니다 꾸벅

      • vinniepaul 2016.10.05 18:05

        아....
        ㅋㅋㅋ
        왜이럴까요 항상 질문을 올리고 나면 해결이 되네요;;;;ㅎㅎㅎ
        웃어야 할지 울어야 할지.. ㅋ
        저는 쿼리를 잘못 이해하고 있었네요
        중간에 오타가 났었나봅니다.
        <update id="deleteFileList" parameterType="hashmap">
        <![CDATA[
        UPDATE TB_FILE SET
        DEL_GB = 'Y'
        WHERE
        BOARD_IDX = #{IDX}
        ]]>
        </update>

        여기서 게시글 아이디에 해당하는 모든 파일들에 대해서 DEL_GB 를 Y 값으로 변경후 리스트맵에 IS_NEW 값이 N 일 경우 IDX 값들을 돌려 DEL_GB 값을 모두 N 으로 변경하는거였네요.. ㅎㅎ;;
        저는 쿼리의 BOARD_IDX = #{IDX} 이부분을 IDX = #{IDX} 로 잘못 써서 이렇게 됐었네요..
        여튼 카루시에라님 포스팅 한지 오래되셨지만 아직도 잘 보고 있습니다 항상 감사드립니다. ㅎㅎ

    • Favicon of http://ㅁㄴ BlogIcon 냠ㅁㄴㅇ 2017.01.09 09:42

      저도 다른분과 마찬가지로 첫번째 파일다운은 잘 되는데, 한번더 받으려고 하면 500에러가뜨네요;;ㅠㅠ

    • 뒹구르르 2017.01.24 16:00

      파일업로드, 다운로드 개념이 많이 부족했는데 큰 도움이 되었습니다. 감사합니다.

    • 이유가 있는 퇴사 2년 2개월째 2017.01.30 15:57

      업로드할때한개씩올리는게아니라여러개를한번에올리면파일몇개이렇게만뜨는데파일명과삭제버튼을목록으로나오게하려면어떻게해야할까요?알려주시면감사하겠습니다ㅜㅜ

    • 무현 2017.03.07 16:55

      서버에 있는 파일도 삭제하고 싶을때는 어떻게 해야할지 감이 안오네요~
      조언 좀 부탁 드립니다. 조언 해주실 분 없나요!!?

    • Favicon of http://jauin.kr BlogIcon 나그네 2017.03.10 09:21

      파일 f = new 파일(파일패스)
      if(f.exists()) f.delete();

      • 초보자 2017.06.28 16:52

        이코드에서
        file[] multiple이거 해서 한번에 파일여러게 업로드 할려면 어떻게 해야 하나요?

    • 초보자 2017.03.30 10:03

      여러개의 첨부 파일을 한번에 일괄로 다운받는 기능은 어떻게 구현하는지 설명 부탁 드려도 될까요~??

      • kim 2017.07.11 20:21

        체크박스 이용해서 체크된 IDX 값을 가지고 처리하시면 될것 같네용~

    • 노규석 2017.06.09 17:40

      아이 엠 그루트

    • 지나가던mysql 2017.07.11 20:08

      기본적으로 정말 잘되어 있는 포스트에 항상 감사드립니다. 스프링 프로젝트는 해본적이 없는데 포스팅만보고도 많이 배운느낌입니다 감사합니다~

    • Sebastian 2017.07.26 11:32

      detail 에서 같은 파일을 페이지 갱신 없이 연속적으로 다운로드 하면 오류가 발생 합니다.
      확인 해 보니 common.js 의 ComSubmit 내용 중 addParam 시에 문제가 발생 합니다.

      매 번 다운로드 링크 클릭시 마다 var comSubmit = new ComSubmit(); 에 의해 ComSubmit 객체는 새로 생성이 되나 기존에 있던 commonForm 은 그대로 살아 있는 상태죠.

      그래서 addParam 을 호출 할 때 2가지 문제가 발생 합니다.
      1) 같은 name 을 가지는 input 필드가 2개 생성됨 (오류는 아님)
      2) 같은 id 를 가지는 input 필드가 2개 생성됨 - DOM 오류 발생

      이를 해결하기 위해서 addParam 을 다음과 같이 수정 했습니다.
      this.addParam = function addParam(key, value){
      if ($("#"+this.formId+" input[name="+key+"]";).length) {
      $("#"+this.formId+" input[name="+key+"]";).val(value);
      }
      else{
      $("#"+this.formId).append($("<input type='hidden' name='"+key+"' id='"+key+"' value='"+value+"' >";));
      }
      };

      같은 name 을 가지는 input 이 있으면 그 필드에 값을 갱신하고 아니면 새로 생성하도록 했습니다.
      (물론 id 가 다른 name[] 배열에 대해서는 대응이 안되는 단점이 있습니다)
      (댓글에 스마일리가 나오네요;; copy-paste 하시면 정상 코드로 나옵니다)

    • Favicon of http://blog.naver.com/jbm5d21 BlogIcon JSOL 2017.08.21 20:34

      본문에 첨부 그림파일 보이게 하고 싶은분들은..
      원초적인 방법 알려 드립니다.

      전자정부 표준 프레임워크의 경우
      server.xml 을 열어보면
      <Context docbase="최상위 패키지명"~>
      이런 줄이 있습니다
      아랫단에 한줄 추가해줍니다.
      <Context docbase="C:/pds/" path="/pds" reloadable="true"/>
      그 다음 Common_SQL.xml 상에 보면 <select id="selectFileInfo"~~ 부분이 있는데요. 여기서 SELECT
      ORIGINAL_FILE_NAME, 아랫단에
      STORED_FILE_NAME, 을 추가합니다.
      그다음
      boardDetail.jsp 에서 다음과 같이 써줍니다.
      <c:forEach var="row" items="${list }">
      <img src="/pds/${row.STORED_FILE_NAME }">
      </c:forEach>

      이러면 게시글에 첨부된 그림파일은 모두 img src로 읽어 들입니다.
      다만 그 외 파일은 엑박이 뜨는데요
      choose 를 추가해주시면 됩니다.

    • 니콜 2017.11.15 14:35

      저장 이미지파일 보기 기능추가 아래참고하세요

      작업1) boardDetail.jsp
      <c:forEach 구문에 img 태그추가

      <img src="http://localhost:8080/first/common/viewImageFile.do?IDX=${row.IDX}" width=100 height=100>

      작업2) CommonController.java 파일에 메소드 추가
      @RequestMapping(value="/common/viewImageFile.do";)
      public void viewImageFile(CommandMap commandMap, HttpServletResponse response) throws Exception {

      // 불러와야할 이미지에 대한 식별값을 파라미터로 받아옴.
      Map<String,Object> map = commonService.selectFileInfo(commandMap.getMap());
      String storedFileName = (String)map.get("STORED_FILE_NAME";);
      //String originalFileName = (String)map.get("ORIGINAL_FILE_NAME";);

      if(storedFileName.indexOf(".jpg";) > -1
      || storedFileName.indexOf(".png";) > -1
      || storedFileName.indexOf(".bmp";) > -1) {



      String pathString = "C:\\dev\\file";
      log.debug("File.separator="+File.separator);
      File file = new File((pathString + File.separator + storedFileName ));
      FileInputStream fis = null;

      BufferedInputStream in = null;
      ByteArrayOutputStream bStream = null;

      try {
      fis = new FileInputStream(file);
      in = new BufferedInputStream(fis);
      bStream = new ByteArrayOutputStream();
      int imgByte;
      while ((imgByte = in.read()) != -1) {
      bStream.write(imgByte);
      }

      // response.setHeader("Content-Type", type);
      response.setContentLength(bStream.size());

      bStream.writeTo(response.getOutputStream());
      response.getOutputStream().flush();
      response.getOutputStream().close();

      } catch (Exception e) {
      e.printStackTrace();
      } finally {
      if (bStream != null) {
      try {
      bStream.close();
      } catch (Exception est) {
      est.printStackTrace();
      }
      }
      if (in != null) {
      try {
      in.close();
      } catch (Exception ei) { ei.printStackTrace(); }
      }

      if (fis != null) {
      try {
      fis.close();
      } catch (Exception efis) {
      efis.printStackTrace();
      }
      }
      }
      }
      }

    • 박종택 2017.12.23 02:43

      글 내용을 보다가 파일다운로드시에 문제가 있는듯하여 글 드립니다. 저는 개인적으로 수정했습니다.
      파일다운로드 버튼을 여러번 누르면 같은 IDX번호에 같은 번호 value가 여러번 붙어 전송시에 에러가 나는 문제점이 있어 제가 $("#commonForm";).empty(); 를 클릭시에 넣어주어 해결했습니다.
      이부분 맞는지 확인 및 답변부탁드립니다. 감사합니다.

    • Favicon of https://chemeez.tistory.com BlogIcon 맛 밀 2018.05.04 14:32 신고

      저도 IDX가 이상하게 넘어와서....... 댓글보고 참고하여 에러 수정 중이었는데
      저는 common.js의 addParam을 수정하여 에러를 잡았습니다.....
      this.addParam = function addParam(key, value){
      if($("#" + this.formId) != null) {
      $("#" + this.formId).empty();
      }
      $("#" + this.formId).append($("<input type='hidden' name='" + key + "' id='" + key + "' value='" + value + "' >";));
      };
      본문에 적어주셨던 reset()부분은 지웠구요...
      이렇게 하는게 문제가 되는지는 잘 모르겠습니다ㅠ_ㅠ

    • 테스트하다 2018.06.14 16:16

      common. js 에 아래와 같이 추가
      this.delParam = function delParam(){
      var del = document.getElementById(this.formId);
      while(del.firstChild) {
      del.removeChild(del.firstChild);
      }
      };



      boardDetail에 fn_downloadFile( obj) 함수를
      function fn_downloadFile(obj){
      var idx = obj.parent().find("#IDX";).val();
      var comSubmit = new ComSubmit();

      comSubmit.setUrl("<c:url value='/common/downloadFile.do' />";);
      comSubmit.addParam("IDX", idx);
      comSubmit.submit();
      comSubmit.delParam(); //사실상 이거만 추가
      }
      같이수정

    • 파비코 2018.11.20 16:43

      1편?부터 여기까지 따라왔습니다 !! 제일 무서운건 역시 오타였어요!! ㅠㅠㅠㅠ 내 한시간..

    • jjo 2018.11.21 15:25

      역시 파일을 두개이상 다운은 안되네요
      문제 역시 addParam에 같은 IDX에 의해 값이 중복되서 그런거같네요
      그래서 기존 코드를 수정하니 되긴하지만 나중에 어떤 문제가 발생할 지 모르겠네요.
      기존 : $("#commonForm";)[0].reset();
      변경 : $("#commonForm";).empty();

      변경된점은 empty로 commomForm의 자식 노드를 DOM에서 그냥 삭제해버리는건데
      이렇게 하니 되긴합니다.
      이렇게 사용하여도 문제 없을려나요?

    • nelav 2019.08.01 09:54

      다중 파일에서 수정으로 이미 올라간 파일들 삭제를 하려고 하는데 버튼을 클릭해도 사라지지가 않는데 원인을 모르겠습니다 :(

      • 찬양도리 2019.08.29 15:58

        자바스크립트 오류는 없는지 디버깅해 보시면 될 듯 합니다.

    • 찬양도리 2019.08.29 16:05

      둘 이상 다운로드 안 되는 현상은, 다운로드 뿐만 아니라 수정하기도 안 되는데, 이 문제는
      이전 글에서 어떤 분이 댓글 올리셨던데,
      $("#commonForm";).children().remove();

      위치가 function fn_downloadFile 함수에서
      new 다음 말고, submit 후에 넣었더니 잘 됩니다.

Designed by Tistory.