관리자 글쓰기

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

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


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


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

1. 파일 업로드

파일 업로드를 위해서 먼저 DB에 파일을 위한 테이블을 만든다.

1.1 SQL

CREATE TABLE TB_FILE1
(
IDX INT PRIMARY KEY AUTO_INCREMENT,
BOARD_IDX INT NOT NULL,
ORIGINAL_FILE_NAME VARCHAR(260) NOT NULL,
STORED_FILE_NAME VARCHAR(36) NOT NULL,
FILE_SIZE INT,
CREA_DTM TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CREA_ID VARCHAR(30) NOT NULL,
DEL_GB VARCHAR(1) NOT NULL DEFAULT 'N'
);

- IDX는 파일의 고유 번호, BOARD_IDX는 게시판의 번호이다. ORIGINAL_FILE_NAME은 그 파일 업로드 시의 파일 이름, STORED_FILE_NAME은 서버에 저장할 때 파일 이름으로 겹치는 이름을 피하고자 만든 컬럼이다.  나머지 컬럼들은 설명 할 필요없다고 생각한다.


1.2 context-common.xml

resource/config/spring 경로에 context-common.xml을 생성해준다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:context="http://www.springframework.org/schema/context"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
                        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
                        http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
                         
    <!-- MultipartResolver 설정 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="100000000" />
        <property name="maxInMemorySize" value="100000000" />
    </bean>
</beans>

- MultipartResolver를 선언해주는 곳으로 파일 업로드 기능을 하는 클래스이다. property의 value 값은 파일 사이즈를 말한다. 


1.3 JSP

이번에는 JSP 파일부터 손보려고 한다. 먼저 게시글 쓰기 페이지에 파일 업로드 하는 부분을 만들 것이다.

<html>
<head>
<title>글쓰기</title>
<%@ include file="/WEB-INF/include/include-header.jspf" %>
</head>
<body>
	<form id="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"  name="TITLE" class="wdp_90" /></td>
				</tr>
				<tr>
					<th scope="row">내용</th>
					<td><textarea cols="100" rows="20" id="CONTENTS" name="CONTENTS" title="내용"></textarea></td>
				</tr>
			</tbody>
		</table>
		<div id="fileDiv">
			<p>
				<input type="file" name="file_0"/>
			</p> 
		</div>
		<br/><br/>
		<a href="#this" id="add" class="btn">파일 추가하기</a>
		<a href="#this" id="list" class="btn">목록으로</a>
		<a href="#this" id="write" class="btn">글쓰기</a>
	</form>
	<%@ include file="/WEB-INF/include/include-body.jspf" %>	
	
	<script type="text/javascript">
		$(document).ready(function(){
			$("#list").on("click",function(e){
				e.preventDefault();
				fn_openBoardList();
			})
			$("#write").on("click",function(e){
				e.preventDefault();
				fn_writeBoard();
			})
		});
		
		
		function fn_openBoardList(){
			var comSubmit = new ComSubmit();
			comSubmit.setUrl("<c:url value='/sample/openBoardList.do'/>");
			comSubmit.submit();
		}
		
		function fn_writeBoard(){
			var comSubmit = new ComSubmit("frm");
			comSubmit.setUrl("<c:url value='/sample/writeBoard.do'/>");
			comSubmit.submit();
		}
	</script>
</body>
</html>

- 주의 깊게 볼 곳은 7, 25~30줄이다.

7줄의 경우는 form의 타입을 설정해줌으로써 multipart 형식이 된다. multipart 형식은 글자를 제외한 그림 같은 파일을 말한다. 25~30줄의 경우는 파일 업로드 시의 사용되는 input을 선언해둔 것이다.



1.4 업로드 확인

- 파일 선택으로 보냈을 때 서버로 전송이 되는지 확인을 Controller와 Service의 코드를 수정하겠다.

SampleController.java

	@RequestMapping(value="/sample/writeBoard.do")
	public ModelAndView writeBoard(CommandMap commandMap, HttpServletRequest req) throws Exception{
		ModelAndView mv = new ModelAndView("redirect:/sample/openBoardList.do");
		sampleService.writeBoard(commandMap.getMap(),req);
				
		return mv;
	}


- 변경된 점은 HttpServletRequest가 파라미터로 추가되고 이 파라미터를 Service 단으로 던져주는 것이다.

우리가 화면에서 전송한 모든 데이터는 HttpServletRequest에 담겨서 전송되고, 그것을 HandlerMethodArgumentResolver를 이용하여 CommandMap에 담아주었다. 그렇지만 첨부파일은 CommandMap에서 처리를 하지 않았기 때문에 HttpServletRequest를 추가로 받도록 하였다. 이 HttpServletRequest에는 모든 텍스트 데이터 뿐만이 아니라 화면에서 전송된 파일 정보도 함께 담겨있다. 우리는 CommandMap을 이용하여 텍스트 데이터는 처리하기 때문에, HttpServletRequest는 파일 정보만 활용할 계획이다. 


SampleService.java

void writeBoard(Map<String,Object> map, HttpServletRequest req) throws Exception;



SampleServiceImpl.java

	@Override
	public void insertBoard(Map<String, Object> map, HttpServletRequest request) throws Exception {
	    sampleDAO.insertBoard(map);
	     
	    MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest)request;
	    Iterator<String> iterator = multipartHttpServletRequest.getFileNames();
	    MultipartFile multipartFile = null;
	    while(iterator.hasNext()){
	        multipartFile = multipartHttpServletRequest.getFile(iterator.next());
	        if(multipartFile.isEmpty() == false){
	            log.debug("------------- file start -------------");
	            log.debug("name : "+multipartFile.getName());
	            log.debug("filename : "+multipartFile.getOriginalFilename());
	            log.debug("size : "+multipartFile.getSize());
	            log.debug("-------------- file end --------------\n");
	        }
	    }
	}
- 로그로 파일의 전송이 잘 이루어지는 지 확인을 위한 코드로 위와 마찬가지로 HttpServletRequest를 받아오고 MultipartHttpServletRequest는 아까 form의 타입에서 multipart 형의 데이터를 관리한다고 했는데 이 데이터를 관리하기 위한 Request이다.

- 다음 반복자를 통해 MultipartHttpServletRequest 안에 포함된 파일 데이터들의 이름들을 이용하여 MultipartFile에 저장하는데 이때 이름은 아까 JSP에서 <input type="file" name="file">이라는 태그를 추가했었다. 여기서 name="file"을 봐야한다.  JSP내에서 작성된 데이터가 서버로 전송될 때에는 태그의 name값을 키(key)로 해서 값(value)가 전송된다. 
즉, request에 값이 전달될때에도 Map과 마찬가지로 key,value 쌍의 형식으로 데이터가 저장된다. 위의 태그에서 name은 "file" 이라는 값이었고, reqeust에서 "file"이라는 키를 통해서 데이터를 가져올 수 있는데, 이 경우 우리는 "file"이라는 키를 알고 있지만, 실제로 개발을 하면, name값은 여러가지 다른 이름으로 넘어올수 있다. 따라서 반복자를 이용하여 파일의 저장하였다.

- 그리고 로그로 파일이 넘어올 때 설정했던 name 값의 key 값과 파일 이름, 파일 크기를 출력하도록 했다.

여기까지 작성하고 테스트를 하자.





이렇게 작성하면 위와 같은 화면이 될 것이다. 위 화면에 파일 선택하여 사진을 골라 제목과 내용을 작성한 뒤 글쓰기를 해보자. 그리고 로그를 확인해보면 밑과 같이 뜰 것이다.



- 위 로그를 보면 정상적으로 서버로 파일이 전송된다는 것을 확인할 수 있다. 그러면 본격적으로 업로드를 하여 서버로 전송하여 서버에서 파일을 받아보자.


2. FileUtils 클래스 생성

java/common/util 경로에 FileUtils, CommonUtils 클래스를 생성하자. 그리고 다음 코드를 작성하자.


CommonUtils.java

public class CommonUtils {
	public static String getRandomString() {
		return UUID.randomUUID().toString().replaceAll("-","");
	}
}

해당 클래스의 메서드는 랜덤한 32글자의 문자열을 만들어 리턴해주는 기능으로 아까 DB에서 STORED_FILE_NAME으로 지정하고 서버에 저장하기 위한 문자열이다.



FileUtils.java

@Component("fileUtils")
public class FileUtils {
	
	String filePath = "C:\\dev1\\file\\";
	Logger log = Logger.getLogger(this.getClass());
	
	public List<Map<String,Object>> parseInsertFileInfo(Map<String,Object> map, HttpServletRequest req) throws Exception{
		MultipartHttpServletRequest mulReq = (MultipartHttpServletRequest)req;
		
		String original_Name=null;
		String original_Extension=null;
		String stored_Name=null;

		MultipartFile mulFile = null;
		Iterator<String> iterator= mulReq.getFileNames();
		
		List<Map<String,Object>> fileList = new ArrayList<Map<String,Object>>();
		Map<String,Object> fileMap = null;
		
		String board_IDX = (String) map.get("IDX").toString();
		
		File file = new File(filePath);
		if(file.exists()==false) {
			file.mkdirs();
		}
		
		while(iterator.hasNext()) {
			mulFile=mulReq.getFile(iterator.next());
			
			if(mulFile.isEmpty()==false) {
				original_Name=mulFile.getOriginalFilename();
				original_Extension=mulFile.getOriginalFilename().substring(original_Name.lastIndexOf("."));
				stored_Name=CommonUtils.getRandomString()+original_Extension;
				
				file = new File(filePath+stored_Name);
				mulFile.transferTo(file);
				
				fileMap = new HashMap<String,Object>();
				
				fileMap.put("BOARD_IDX", board_IDX);
				fileMap.put("ORIGINAL_FILE_NAME", original_Name);
				fileMap.put("STORED_FILE_NAME", stored_Name);
				fileMap.put("FILE_SIZE", mulFile.getSize());
				fileList.add(fileMap);
			}
		}
		
		return fileList;
	}
}
- @Component 어노테이션을 이용하여 이 객체의 관리를 스프링이 담당하도록 할 계획이다.

- 다음은 아까 위에서 작성한 ServiceImpl와 비슷한 부분이 많이 보인다. 해당 파일의 게시물의 번호를 가져오는 부분도 있다.
이후 원본 확장자명을 구분한 뒤 stored_Name에 붙여 해당 파일을 서버의 윗부분에서 선언한 경로에 전송한다. 그 뒤 파일들을 각각 맵에 저장한 뒤 리스트에 추가하여 해당 리스트를 반환해준다.

다음으로 ServiceImpl 부분을 다시 수정하겠다.

SampleServiceImpl.java
	@Resource(name="fileUtils")
	private FileUtils fileUtils;

	@Override
	public void writeBoard(Map<String, Object> map, HttpServletRequest req) throws Exception {
		sampleDAO.writeBoard(map);
		
		List<Map<String,Object>> list = fileUtils.parseInsertFileInfo(map,req);
		
		for(int i=0; i<list.size();i++) {
			sampleDAO.insertFile(list.get(i));
		}

	}

- 아까 위에서 만든 스프링 객체를 자동주입으로 선언하고 작성한 메서드를 이용하여 파일이 포함된 리스트를 반환 받는다. 그리고 반환 받은 파일 리스트 하나하나를 DAO를 통해 DB에 저장한다. 


SampleDAO.java

	public void insertFile(Map<String, Object> map) {
		insert("sample.insertFile",map);
	}

- 다른 DAO 메서드와 크게 차이는 없다.


sample_SQL.xml

     <insert id="sample.insertFile" parameterType="hashmap">
     	<![CDATA[
     		INSERT INTO TB_FILE1
     		(
     			BOARD_IDX,
     			ORIGINAL_FILE_NAME,
     			STORED_FILE_NAME,
     			FILE_SIZE,
     			CREA_ID
     		)
     		VALUES
     		(
     			#{BOARD_IDX},
     			#{ORIGINAL_FILE_NAME},
     			#{STORED_FILE_NAME},
     			#{FILE_SIZE},
     			'ADMIN'
     		)
     	]]>
     </insert>

- 아까 FileUtils 클래스에서 #{BOARD_IDX} 등 4개의 값들을 선언하여 DAO를 통해 보내주었기 때문에 여기서 사용이 가능하다.

그리고 다음으로 sample.writeBoard 부분 을 고쳐야한다. 다음과 같이 고쳐주자.

     <insert id="sample.writeBoard" parameterType="hashmap" useGeneratedKeys="true" keyProperty="IDX" >
     	<![CDATA[
     		INSERT INTO
     			TB_BOARD1(
     				IDX,
     				TITLE,
     				CONTENTS,
     				HIT_CNT,
     				DEL_GB,
     				CREA_ID)
     			VALUES(
     				#{IDX},
     				#{TITLE},
     				#{CONTENTS},
     				0,
     				'N',
     				'ADMIN')
       	]]>
     </insert>

- useGeneratedKeys, keyProperty  의 속성을 추가하였는데 useGeneratedKeys 란 자동 생성키를 사용하겠느냐로 여기서 만들어진 IDX, 즉 게시물 번호를 말한다. keyProperty가 그 해당 자동 생성키의 이름을 설정하는 것으로 이를 통해 위에 FileUtils에서 Board_IDX 값을 받아왔다.


여기까지 작성 후 테스트 하여 보자.



- 다음과 같이 작성하여 글쓰기를 처리하였다.



- 별 문제 없이 게시물 리스트에 등록됨을 확인하였다.



- 로그를 통해서도 파일이 잘 전송되는 것을 확인할 수 있다.



- DB에서도 해당 파일의 정보가 삽입된 것을 확인할 수 있다.


- 마지막으로 아까 정해둔 경로를 통해 랜덤한 문자열로 등록된 이미지 파일을 볼 수 있다.