관리자 글쓰기

본 내용은 혼자 공부하기 위하여 다른 포스트를 보면서 저에게 필요한 부분과 궁금했던 부분을 추가하여 게시하는 곳입니다.

궁금한 것에 대한 것은 모르는 것이 많겠지만 함께 알아보도록 노력하겠습니다.


참조 게시 포스트 : http://addio3305.tistory.com/


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

1. 파일 다운로드 화면

전에 작성한 업로드는 데이터베이스에 파일의 정보를 담아두고 서버에 파일을 저장하는 방식이다. 먼저 사용자에게 파일을 정보와 파일을 받을 수 있는 메뉴부터 만들어 보자.

1.1 Controller

SampleController.java

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

- 먼저 Controller에서 게시글 상세보기에 대한 코드를 위와 같이 수정한다. map을 다시 map과 list를 구분한 것은 map은 게시글의 정보가 담긴 것이고 list는 해당 게시물의 파일들의 정보를 담아놓은 것이다. 미리 Service단에서 구분하여 map에 키를 추가 해놓은 것이다. 


1.2 Service

SampleServiceImpl.java

	@Override
	public Map<String, Object> selectBoard(Map<String, Object> map) throws Exception {
		sampleDAO.updateHitCnt(map);
		
		Map<String, Object> resultMap = new HashMap<String,Object>();
		resultMap.put("map", sampleDAO.selectBoard(map));
		resultMap.put("list", sampleDAO.selectFileList(map));
		
		return resultMap; 
	}

- 새로운 Map을 만들어서 각각의 게시물의 정보와 파일들의 정보를 키를 "map"과 "list"로 추가하고 반환한다. 여기서 DAO단의 selectFileList(map) 메서드가 있는데 이 부분에서 파일들의 정보를 담은 리스트를 가져오는 것이다.


1.3 DAO

SampleDAO.java

	@SuppressWarnings("unchecked")
	public List<Map<String,Object>> selectFileList(Map<String, Object> map) {
		return (List<Map<String,Object>>)selectList("sample.selectFileList",map);
	}

- 게시물 리스트를 불러오는 것과 비슷한 형식으로 사용된 것을 보여준다. 파일 리스트의 정보를 가져오는 것이기 때문에 전혀 다를 것이 없기 때문이다. 해당 map에는 게시물의 번호가 들어가 있다.


1.4 SQL

sample_SQL.xml

     <select id="sample.selectFileList" parameterType="hashmap" resultType="hashmap">
     	<![CDATA[
     		SELECT
     			IDX,
     			ORIGINAL_FILE_NAME,
     			ROUND(FILE_SIZE/1024) AS FILE_SIZE
     		FROM
     			TB_FILE1
     		WHERE
     			BOARD_IDX=#{IDX} AND
     			DEL_GB='N'
     	]]>
     </select>

- 받아온 게시물 번호를 넣어 게시물에 해당하는 파일들을 가져오는 SQL이다. 6번줄은 파일의 크기를 MB로 표현하기 위하기 위함이다.


1.5 JSP

boardDetail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>상세보기</title>
<%@ include file="/WEB-INF/include/include-header.jspf" %>
</head>
<body>
	<table class="board_view">
		<caption>상세보기</caption>
		<colgroup>
			<col width="15%">
			<col width="35%">
			<col width="15%">
			<col width="*">
		</colgroup>
		
		<tbody>
			<tr>
				<th>제목</th>
				<td>${map.TITLE}</td>
				<th>조회수</th>
				<td>${map.HIT_CNT }</td>
			</tr>
			<tr>
				<th>작성자</th>
				<td>${map.CREA_ID }</td>
				<th>작성시간</th>
				<td>${map.CREA_DTM }</td>
			</tr>
			<tr>
				<th>내용</th>
				<td colspan="3">
					${map.CONTENTS }
				</td>
			</tr>
			<tr>
				<th>첨부파일</th>
				<td colspan="3">
					<c:forEach items="${list}" var="row" >
						<p>
							<input type="hidden" value="${row.IDX }" id="IDX">
							<a href="#this" name="file">${row.ORIGINAL_FILE_NAME }</a>
							(${row.FILE_SIZE }Byte)
						</p>
					</c:forEach>							
			</tr>
		</tbody>
	</table>
	<a href="#this" id="list" class="btn">목록으로</a>
	<a href="#this" id="modify" class="btn">수정하기</a>
	<%@ include file="/WEB-INF/include/include-body.jspf" %>
	
	<script type="text/javascript">
		$(document).ready(function(){
			$("#list").on("click",function(e){
				e.preventDefault();
				fn_openBoardList();
			})
			$("#modify").on("click",function(e){
				e.preventDefault();
				fn_openBoardModify();
			})
			$("a[name='file']").on("click",function(e){
				e.preventDefault();
				fn_fileDownload($(this));
			})
		})
		
		function fn_openBoardList(){
			var comSubmit = new ComSubmit();
			comSubmit.setUrl("<c:url value='/sample/openBoardList.do'/>");
			comSubmit.submit();
		}
		function fn_openBoardModify(){
			var idx = "${map.IDX}";
			var comSubmit = new ComSubmit();
			comSubmit.setUrl("<c:url value='/sample/openBoardModify.do'/>");
			comSubmit.addParam("IDX",idx);
			comSubmit.submit();
		}
		function fn_fileDownload(obj){
			var comSubmit = new ComSubmit();
			comSubmit.setUrl("<c:url value='/common/downloadFile.do'/>");
			comSubmit.addParam("IDX",obj.parent().find("#IDX").val());
			comSubmit.submit();
			$("#commonForm").children().remove();
		} 
	</script>	
</body>
</html>

- 38~48줄

Controller에서 넘겨준 list에 담긴 파일 정보와 함께 파일 다운로드 메뉴와 함께 작성하였다. 해당 idx는 게시물의 idx가 아니라 파일 idx이고, 파일 크기와 원본 이름을 표시하게 하였다. 이 방식은 게시물 리스트에서 상세보기로 들어갈 때의 처리와 비슷하다. 게시물 리스트에선 클릭한 게시물의 IDX를 jQuery로 넘겨주기 위하여 사용되었지만 여기에선 클릭한 파일의 IDX를 jQuery로 넘겨주기 위하여 사용되었다.


- 65~68, 83~89줄

해당 부분도 게시물 상세보기와 거의 동일하다. $("#commonForm").children().remove() 이 부분은 파일 다운로드 1회 뒤에 다시 다운로드를 위하여 파일 클릭 시에 addParam을 통해서 들어가는 파라미터가 배열로 들어가기 때문에 사용했던 파라미터들의 처리를 지워주기 위하여 사용했다. IDX는 게시물 번호를 말하는 것이다.


1.6 중간 테스트

현재까지 한 것을 테스트하면 이렇다.


게시글 상세보기 화면과 처리에 대한 로그이다. 물론 다운로드의 클릭에 대한 것은 아직 작성하지 않아 오류가 발생한다. 로그도 정상적으로 잘 처리됨을 볼 수 있다. 이제 다운로드의 실질적인 것을 작성해보자.


2. 다운로드

다운로드의 방식은 

1. 클라이언트의 파일 다운로드 요청 -> 2. 서버에서 해당 요청을 받음 -> 3. 서버에서 요청에 해당되는 파일의 정보를 DB에 요청 -> 4. DB에서 해당 파일의 정보를 검색 -> 5. 해당 파일 정보를 서버로 넘겨줌 -> 6. 서버에서 해당 정보를 통해 파일 저장 경로의 파일을 가져옴 -> 7. 서버에서 가져온 파일 데이터를 클라이언트에게 보내줌

이런 과정을 거침으로써 다운로드가 완료되는 것이다. 이 과정을 처리하는 코드를 작성할 것이다.


2.1 Controller

CommonController.java

@Controller
public class CommonController {
	
	@Resource(name="commonService")
	private CommonService commonService;
	
	@RequestMapping(value="/common/downloadFile.do")
	public void downloadFile(CommandMap commandMap, HttpServletResponse response) throws Exception{
		Map<String,Object> map = commonService.selectFileInfo(commandMap.getMap());
		String original_File_Name = (String)map.get("ORIGINAL_FILE_NAME");
		String stored_File_Name = (String)map.get("STORED_FILE_NAME");
		
		byte[] fileByte = FileUtils.readFileToByteArray(new File("C:\\dev1\\file\\"+stored_File_Name));
		
	    response.setContentType("application/octet-stream");
	    response.setContentLength(fileByte.length);
	    response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(original_File_Name,"UTF-8")+"\";");
	    response.setHeader("Content-Transfer-Encoding", "binary");
	    response.getOutputStream().write(fileByte);
	     
	    response.getOutputStream().flush();
	    response.getOutputStream().close();

	}
}
- Controller의 어노테이션을 해놓았고, SampleController에서 봤던 양식과 동일하다. CommonService라는 스프링 객체를 가져왔다. 위의 jsp 파일의 jQuery에서 setUrl에 등록했던 주소를 매핑하는 것을 볼 수 있다. (@RequestMapping(value="/common/downloadFile.do")). 또 특별한 것은 HttpServletResponse 파라미터로 해당 파라미터는 서버에서 클라이언트로 응답할 때(데이터를 전송할 때) 사용하는 것으로 Request의 경우는 클라이언트에서 서버로 요청 하는 것과 반대의 개념으로 생각하면 된다. 이를 통해 파일을 클라이언트에게 전송할 때 사용한다. 

-다음 Service 단에서 만든 map을 할당 받아 원본 파일명과 저장된 파일명을 가져오고 저장된 파일명을 이용하여 파일이 저장된 경로를 통해 파일을 가져와 byte[]로 변환한다. 업로드에서 사용하였던 FileUtils 클래스가 아닌 org.apache.commons.io 패키지의 FileUtils 클래스이다.(해당 라이브러리는 pom.xml에 선언 했기에 사용할 수 있다.)

- 다음 생소한 부분의 코드가 있다. 읽어온 파일 정보를 화면에서 다운로드 할 수 있게 변환하는 부분이다. 보통 다운로드를 할 땐 이렇게 여러가지 설정이 담겨져있다.(15~22)


- 15줄

웹서버는 브라우저로 전송될 페이지가 html 인경우 text/html을 표준 MIME 타입으로 지정합니다. 그러나 필요에 의해서 이 MIME 타입을 변경하고자 할 경우나 또는 캐릭터의 인코딩셋을 변경하고자 할때 setContentType 메소드를 사용할 수 있습니다. octet-stream 이라는 놈은 이름 그대로 8비트 바이너리 배열을 의미하며 http나 이메일상에서 application 형식이 지정되지 않았거나 형식을 모를때 사용합니다. 결국 브라우저는 octet-stream 으로 MIME 타입이 지정된 경우 단지 바이너리 데이터로서 다운로드만 가능하게 처리하게 됩니다.


- 17줄

Content-Disposition 속성을 이용하여 데이터의 형식을 지정할 수 있는데 attachment로 지정되어 있는데 첨부 파일을 말한다. 그 뒤에 fileName이라고 적혀있는 것은 다운로드 할 때 기본으로 적혀있던 파일 이름을 말한다. 그 뒤에 인코더 되있는 부분은 한글파일의 경우 UTF-8로 인코딩 되어 있지 않으면 깨진 이름으로 나오는데 이를 방지하기 위함이다. attachment;와 fileName 사이에는 띄워쓰기를 꼭 해주어야 하고 인코드하는 뒤 부분 "\" 부분도 꼭 써줘야 다운로드가 가능하다.


- 18줄

Content-Transfer-Encoding 는 전송되는 데이터의 안의 내용물들의 인코딩 방식을 말하며 여기에선 binary 방식을 택한 것이다.


- 19줄

위에서 byte[] 타입으로 변환한 파일을 response를 통해 클라이언트로 보내준다.


- 21~22줄

response를 중지하고 닫아준다.


2.2 Service

CommonService.java

public interface CommonService {

	public Map<String, Object> selectFileInfo(Map<String, Object> map) throws Exception;
}


CommonServiceImpl.java

@Service("commonService")
public class CommonServiceImpl implements CommonService {

	@Resource(name="commonDAO")
	private CommonDAO commonDAO;
	
	@Override
	public Map<String, Object> selectFileInfo(Map<String, Object> map) throws Exception {
		return commonDAO.selectFileInfo(map);
	}

}


- SampleService 와 동일한 형태를 하고 있다. 파일에 대한 정보를 가져오는 메서드가 선언되어있다.


2.3 DAO

CommonDAO.java

@Repository("commonDAO")
public class CommonDAO extends AbstractDAO {

	@SuppressWarnings("unchecked")
	public Map<String, Object> selectFileInfo(Map<String, Object> map) {
		return (Map<String,Object>)selectOne("common.selectFileInfo",map);
	}
}
- SampleDAO 와 동일한 형태를 하고 있다. 그리고 파일의 정보를 가져온다는 점에서도 동일한 기능을 하는 것이 있다. 다른 점이라고는 SQL을 호출할 때의 namespace를 common으로 지정하였다. 파일 다운로드와 관련된 기본적인 기능으로 common으로 따로 두었다.

2.4 SQL
common_SQL.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="common">

	<select id="selectFileInfo" parameterType="hashmap" resultType="hashmap">
    <![CDATA[
        SELECT
            STORED_FILE_NAME,
            ORIGINAL_FILE_NAME
        FROM
            TB_FILE1
        WHERE
            IDX = #{IDX}
    ]]>
</select>


</mapper>


- namespace를 common으로 지정하였고, selectFileInfo에서 IDX를 사용할 수 있는데 이는 상세보기 화면 구성 jsp에서 addParam을 통해 파라미터를 추가해주었을 때의 IDX 값이다.


2.5 테스트

여기까지 작성을 완료하였다면 이제 테스트를 해보자.



- 작성자는 크롬이라 따로 다운로드 화면은 뜨지 않지만 익스플로러의 경우는 익숙한 화면이 뜰 것이다.



- 로그를 보면 정상적으로 SQL이 작동하고 해당 파일의 정보가 로그에 적혀있는 것을 볼 수 있다.