ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링(Spring) 개발 - (15) 파일 업로드 & 다운로드 (3/3)
    Spring 2015. 8. 3. 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





    댓글

Designed by Tistory.