파일업로드 이해하기
웹에서는 이 클라이언트/서버 간 요청/응답을 HTTP 프로토콜로 진행한다.
HTTP에서는 파일도 지원해준다.
파일업로드란 클라이언트가 요청에 파일을 포함하고
서버가 요청받은 파일을 처리하는 과정의 일환이다.
클라이언트 : "서버야, 나 Request보낼 때 파일도 포함시켜 보낼게. 이거 서버에 저장해줘"
서버 : "OK. 어디보자. Request에 파일 있군. 알았어 잘 처리했어."
의 과정이다 . 물론 위의 대화를 HTTP프로토콜에서 처리해야 되는데 이게 생각보다 어렵다.
우선 파일업로드를 위해선 다음의 3가지 규칙을 꼭 지켜줘야 한다.
- <input type="file" > (파일을 선택해야되니까..)
- <form> 태그 method는 POST (밑에서 설명)
- <form> 태그 enctype=multipart/form-data (밑에서 설명)
Multipart란
HTTP에서 request는 header와 body부분으로 나누어져있다.
header의 content-type은 body에 대한 데이터를 정의한다.
서버는 content-type의 값을 보고 body를 알맞은 형태로 해석한다.
이 요청 헤더 content-type의 한 종류로서 웹 클라이언트가 요청을 보낼 때,
HTTP 요청의 바디부분을 여러부분으로 나눠서 보내는 방식이다.
Multipart가 생긴 이유
파일을 업로드 할 때, 사진 설명을 위한 input (type="text")과
사진을 위한 input(type="file") 2개가 들어간다고 가정합니다.
이 두 input 간에 Content-type은 사진 설명은 application/x-www-form-urlencoded 이 될 것이고,
사진 파일은 image/jpeg입니다. 즉 하나의 요청에 Content-type이 서로 다른것이 2개가 있습니다.
두 종류의 데이터가 하나의 HTTP Request Body에 들어가야 하는데,
하나의 Body에서 이 2 종류의 데이터를 구분에서 넣어주는 방법이 필요해졌습니다.
그래서 등장한 것이 multipart 타입입니다.
이렇게 Body에서 이 데이터를 구분해야하기 때문에
요청파라미터를 url뒤에 문자열로 추가하는 GET방식으로는 파일을 보낼 수 없습니다.
그래서 multipart타입은 POST방식에서만 사용가능합니다.
Multipart를 사용한 요청헤더
<form enctype="multipart/form-data" method="post">
<input id="desc" type="text" />
<input id="image" type="file" />
</form>
위의 form으로 데이터를 보냈을 때 request 요청은 다음과 같이 보내진다.
POST /someurl HTTP/1.1
...
...
Content-type: multipart/form-data; boundary=---------------------------7d81c536d04c8 <- Content-type
...
...
text데이터
-----------------------------7d81c536d04c8 <- input text의 데이터
Content-Disposition: form-data; name="desc"
사진
-----------------------------7d81c536d04c8 <- input file의 데이터
Content-Disposition: form-data; name="image"; filename="fileName.jpg"
Content-Type: image/jpeg
...(Binary 이미지 데이터)
Content-type: multipart/form-data; boundary=---------------------------7d81c536d04c8을 보자.
Content-type: multipart/form-data로 요청을 보내면
웹 클라이언트(브라우저)는 boundary라는 걸 생성한다.
이 boundary라는 걸 이용해서 요청 body를 여러 부분으로 나누게 된다.
서버는 맨 처음 Content-type이 multipart/form-data;라는 걸 보고
boundary값을 통해 데이터가 나뉘어져 온다는 걸 인식하고 그에 맞게 해석하게 될 것이다.
Spring으로 파일요청 처리하기 1-업로드
파일저장을 위한 FileUtils 관련 디펜던시 추가
<!-- commons io -->
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
Spring에서 MultipartResolver 설정
기본적으로 요청을 처리하는 것이기 때문에 서버에서 HttpServletRequest 객체를 분석하면 된다.
body부분을 읽어들이는 inputStream을 통해 파일 및 데이터를 처리하면 된다.
그런데 직접 inputStream을 통해 파일을 처리하는 것이 힘드니까
Spring은 이 파일을 처리할 수 있는 기능을 제공한다.
Spring이 기본으로 제공하는 MultipartResolver는 다음의 두 개가 있다.
- org.springframework.web.multipart.commons.CommonsMultipartResolver
- org.springframework.web.multipart.support.StandardServletMultipartResolver
위 두 MultipartResolver 구현체 중 하나를 스프링 빈으로 등록해주면 된다.
주의할 점은 빈의 id가 반드시 "multipartResolver"여야한다는 점이다.
DispatcherServlet은 내부적으로 이름이 "multipartResolver"인 빈을 사용하기 때문에
다른 이름으로 빈을 등록할 경우 MultipartResolver로 사용되지 않는다.
이 중 나는 StandardServletMultipartResolver를 이용해 파일업로드를 하려고한다. StandardServletMultipartResolver는 Servlet 3.0 버전 이후에 사용할 수 있다.
설정은 크게 2가지를 하면 된다.
- DispatcherServlet이 서블릿3의 Multipart를 처리하도록 설정(web.xml)
- StandardServletMultipartResolver 클래스를 MultipartResolver로 설정 (mvc-servlet.xml)
web.xml에 DispatcherServlet 의 Multipart 설정
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/classes/spring/mvc-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<multipart-config>
<max-file-size>31457280</max-file-size> <!-- 30mb-->
<max-request-size>1004857600</max-request-size> <!-- 100mb -->
</multipart-config>
</servlet>
<multipart-config >의 설정 태그
태그 | 설명 |
<location> | 업로드 한 파일이 임시로 저장될 위치를 지정한다. |
<file-size-threshold> | 업로드 한 파일 크기가 이 태그의 값보다 크면 에서 지정한 디렉토리에 임시로 파일을 생성한다. 업로드 파일 크기가 이 태그의 값 이하면 메모리에 파일 데이터를 보관한다. 단위는 바이트이며, 기본 값은 0이다 |
<max-file-size> | 업로드 가능한 파일의 최대 크기를 바이트 단위로 지정한다. -1은 제한없음을 의미하며 기본값은 -1이다. |
<max-request-size> | 전체 Multipart 요청 데이터의 최대 제한 크기를 바이트 단위로 지정한다. -1은 제한없음을 의미하며 기본값은 01이다. |
mvc-servlet.xml에 설정 (DispatcherServlet 설정파일)
<!--빈 이름은 무조건 multipartResolver -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"></beans:bean>
글 등록 시 첨부파일 추가
freeForm.jsp
freeForm.jsp에서 파일을 추가하도록 <input type="file">태그를 추가하고,
또 파일추가, 삭제를 위해 javascript를 추가해줍시다.
<tr>
<th>첨부파일
<button type="button" id="id_btn_new_file">추가</button>
</th>
<td class="file_area">
<div class="form-inline">
<input type="file" name="boFiles" class="form-control">
<button type="button" class="btn_delete btn btn-sm">삭제</button>
</div>
</td>
</tr>
<script type="text/javascript">
$('#id_btn_new_file').click(function(){
$('.file_area').append('<div class="form-inline">'
+ '<input type="file" name="boFiles" class="form-control">'
+ ' <button type="button" class="btn_delete btn btn-sm">삭제</button>' + '</div>');
});
$('.file_area').on('click','.btn_delete', function(){
$(this).closest('div').remove();
});
</script>
이렇게 <input type="file"> 을 추가하면 저장버튼을 눌렀을 때 첨부파일이 요청에 포함되서 날라가야한다.
그렇게 되도록 <form> 태그에 enctype을 바꾸자.
<form:form action="freeRegist.wow" method="post" modelAttribute="freeBoard" enctype="multipart/form-data">
여기까지 하면 freeForm.jsp에서 요청은 올바로 될 것이다.
이제 파일을 포함한 요청을 Controller에서 처리하면 된다.
FreeBoardController
@PostMapping("/free/freeRegist.wow")
public String freeRegist(Model model
,@ModelAttribute("freeBoard")@Validated(Default.class)
FreeBoardVO freeBoard
,BindingResult error
,@RequestParam(value = "boFiles",required = false)MultipartFile[] boFiles) throws IOException {
if(error.hasErrors()) {
return "free/freeForm";
}
try {
if(boFiles!=null) {
//boFiles를 서버컴퓨터에 업로드 하는 코드
for (MultipartFile multipart : boFiles) {
if (!multipart.isEmpty()) {
String fileName = UUID.randomUUID().toString(); //파일이름은 랜덤해야됨. 사용자가올리는 다른 파일이름이 같을 수 있음.
String filePath = "C:\\workspace\\upload" + File.separatorChar + "free";
FileUtils.copyInputStreamToFile(multipart.getInputStream(), new File(filePath, fileName));
// 여기서 실제 파일이 저장(regist에서 실행됬다), inputStream을 file로 변환하는 메소드
// multipart.transferTo(new File(filePath, fileName)); // 비슷한 역할
}
}
}
freeBoardService.registBoard(freeBoard); //이미 파일은 업로드 됨. 업로드 된 파일정보를 DB에 AttachVO로 등록.
ResultMessageVO resultMessageVO = new ResultMessageVO();
resultMessageVO.messageSetting(true, "글 등록 성공", "글을 등록했습니다.", "/free/freeList.wow", "목록으로");
model.addAttribute("resultMessageVO", resultMessageVO);
return "common/message";
} catch (BizNotEffectedException ebe) {
ResultMessageVO resultMessageVO = new ResultMessageVO();
resultMessageVO.messageSetting(false, "등록실패", "등록에 실패했습니다.", "/free/freeList.wow", "목록으로");
model.addAttribute("resultMessageVO", resultMessageVO);
return "common/message";
}
}
여기까지하면
FreeForm에서 파일 선택 후 freeRegist로 요청했을 때 파일이 업로드된다.
Spring으로 파일요청 처리하기2-DB에 업로드된 파일정보 저장 후 활용하기
DB에 테이블 만들기, AttachVO 만들기, FreeBoardVO에 필드추가
먼저 ATTACH 테이블을 만들어줍시다.
CREATE TABLE attach(
atch_no NUMBER NOT NULL, -- 첨부파일 번호(PK)
atch_parent_no NUMBER NOT NULL, -- 부모글의 PK
atch_category VARCHAR2(30) NOT NULL, -- 상위글 구분 (BOARD, FREE, QNA, PDS 등 )
atch_file_name VARCHAR2(100), -- 실제 저장된 파일명
atch_original_name VARCHAR2(200) NOT NULL, -- 사용자가 올린 원래 파일명
atch_file_size NUMBER DEFAULT 0, -- 파일 사이즈
atch_fancy_size VARCHAR2(10), -- 팬시 사이즈
atch_content_type VARCHAR2(100), -- 컨텐츠 타입
atch_path VARCHAR2(100) , -- 저장 경로 (board/2020)
atch_down_hit NUMBER(10) DEFAULT 0, -- 다운로드 횟수
atch_del_yn CHAR(1) DEFAULT 'N', -- 삭제여부 (별도 스케쥴에 의해서 삭제처리)
atch_reg_date DATE DEFAULT SYSDATE, -- 등록일
CONSTRAINT pk_attach PRIMARY KEY (atch_no)
);
COMMENT ON TABLE attach is '첨부파일 테이블';
COMMENT ON COLUMN attach.atch_no IS '첨부파일 번호(PK)';
COMMENT ON COLUMN attach.atch_parent_no IS '부모글의 PK ';
COMMENT ON COLUMN attach.atch_category IS '상위글 분류(BOARD, FREE, QNA, PDS 등)';
COMMENT ON COLUMN attach.atch_file_name IS '실제 저장된 파일명';
COMMENT ON COLUMN attach.atch_original_name IS '사용자가 올린 원래 파일명';
COMMENT ON COLUMN attach.atch_file_size IS '파일 사이즈';
COMMENT ON COLUMN attach.atch_fancy_size IS '팬시 사이즈';
COMMENT ON COLUMN attach.atch_content_type IS '컨텐츠 타입';
COMMENT ON COLUMN attach.atch_path IS '저장 경로(board/2020) ';
COMMENT ON COLUMN attach.atch_down_hit IS '다운로드 횟수';
COMMENT ON COLUMN attach.atch_del_yn IS '삭제여부(스케쥴에 의해서 파일삭제처리)';
COMMENT ON COLUMN attach.atch_reg_date IS '등록일';
-- 첨부파일 번호(PK)를 위한 시퀀스 생성
CREATE SEQUENCE seq_attach ;
AttachVO를 만들어줍시다. atachFileSize 파일사이즈가 long인 거에 주의하자.
package com.study.attach.vo;
import java.io.Serializable;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class AttachVO {
private int atchNo; /* 첨부파일 번호(PK) */
private int atchParentNo; /* 부모글의 PK */
private String atchCategory; /* 상위글 분류(BOARD, FREE, QNA, PDS 등) */
private String atchFileName; /* 실제 저장된 파일명 */
private String atchOriginalName; /* 사용자가 올린 원래 파일명 */
private long atchFileSize; /* 파일 사이즈 1024, 1024*1024 */
private String atchFancySize; /* 팬시 사이즈 : 1KB , 1MB */
private String atchContentType; /* 컨텐츠 타입 */
private String atchPath; /* 저장 경로(board/2020) */
private int atchDownHit; /* 다운로드 횟수 */
private String atchDelYn; /* 삭제여부(스케쥴에 의해서 파일삭제처리) */
private String atchRegDate; /* 등록일 */
//get/set, toString
}
FreeBoardVO에 다음의 필드를 추가해 줍니다.
private List<AttachVO> attaches ; /*첨부파일 리스트 */
private int[] delAtchNos; /*삭제를 위한 글 번호 */
//get/set
IAttachDao, attach.xml
IattachDao
package com.study.attach.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.study.attach.vo.AttachVO;
@Mapper
public interface IAttachDao {
/** 첨부파일 등록 */
public int insertAttach(AttachVO attach);
}
attach.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="com.study.attach.dao.IAttachDao">
<insert id="insertAttach" parameterType="com.study.attach.vo.AttachVO">
INSERT INTO attach (
atch_no , atch_parent_no , atch_category
, atch_file_name , atch_original_name , atch_file_size
, atch_fancy_size , atch_content_type , atch_path
, atch_down_hit , atch_del_yn , atch_reg_date
) VALUES (
<![CDATA[
seq_attach.nextval, #{atchParentNo} , #{atchCategory}
, #{atchFileName} , #{atchOriginalName}, #{atchFileSize}
, #{atchFancySize} , #{atchContentType} , #{atchPath}
,0 , 'N' , SYSDATE
]]>
)
</insert>
</mapper>
MultipartFile을 처리할 StudyAttachUtils
저장할 경로 설정 appconfig.properties
#FileUpload
#File Upload (developer)
#path depend on windows or linux , if your os is windows, the path would start with C:\\
# linux /home/ssam/upload
file.upload.path=/home/ssam/upload
<input type=file> 로 날라온 파일을 경로에 업로드 하는 studyAttachUtils이다.
package com.study.common.util;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import com.study.attach.vo.AttachVO;
@Component
public class StudyAttachUtils {
@Value("#{util['file.upload.path']}")
private String uploadPath;
/** 다중 MultipartFile에서 VO 설정 및 업로드 파일 처리 후 List 리턴 */
public List<AttachVO> getAttachListByMultiparts(MultipartFile[] boFiles, String category, String path)
throws IOException {
List<AttachVO> atchList = new ArrayList<AttachVO>();
for (int i = 0; i < boFiles.length; i++) {
MultipartFile multipart = boFiles[i];
AttachVO vo = this.getAttachByMultipart(multipart, category, path);
if (vo != null) {
atchList.add(vo);
}
}
return atchList;
}
/** MultipartFile에서 VO 설정 및 업로드 파일 처리 후 리턴, 없는 경우 null */
public AttachVO getAttachByMultipart(MultipartFile multipart, String category, String path) throws IOException {
if (!multipart.isEmpty()) {
String fileName = UUID.randomUUID().toString();
AttachVO vo = new AttachVO();
vo.setAtchOriginalName(multipart.getOriginalFilename());
vo.setAtchFileSize(multipart.getSize());
vo.setAtchContentType(multipart.getContentType());
vo.setAtchFileName(fileName);
vo.setAtchCategory(category);
vo.setAtchPath(path);
vo.setAtchFancySize(fancySize(multipart.getSize()));
String filePath = uploadPath + File.separatorChar + vo.getAtchPath();
FileUtils.copyInputStreamToFile(multipart.getInputStream(), new File(filePath, fileName));
// 여기서 실제 파일이 저장(regist에서 실행됬다), inputStream을 file로 변환하는 메소드
// multipart.transferTo(new File(filePath, fileName)); // 비슷한 역할
return vo;
} else {
return null;
}
}
private DecimalFormat df = new DecimalFormat("#,###.0");
private String fancySize(long size) {
if (size < 1024) { // 1k 미만
return size + " Bytes";
} else if (size < (1024 * 1024)) { // 1M 미만
return df.format(size / 1024.0) + " KB";
} else if (size < (1024 * 1024 * 1024)) { // 1G 미만
return df.format(size / (1024.0 * 1024.0)) + " MB";
} else {
return df.format(size / (1024.0 * 1024.0 * 1024.0)) + " GB";
}
}
}
등록과정
FreeBoardController
@Autowired
private StudyAttachUtils fileService;
@PostMapping("/free/freeRegist.wow")
public String freeRegist(Model model, FreeBoardVO freeBoard
, @RequestParam(name = "boFiles", required = false) MultipartFile[] boFiles) throws BizException,IOException {
if(boFiles!=null) {
List<AttachVO> attaches = fileService.getAttachListByMultiparts(boFiles, "FREE", "free");
freeBoard.setAttaches(attaches);
}
ResultMessageVO resultMessageVO = new ResultMessageVO();
freeBoardService.registBoard(freeBoard);
resultMessageVO.messageSetting(true, "등록", "등록성공", "/free/freeList.wow", "목록으로");
model.addAttribute("resultMessageVO", resultMessageVO);
return "common/message";
}
FreeBoardServiceImpl
@Override
public void registBoard(FreeBoardVO freeBoard) throws BizNotEffectedException {
int cnt = freeBoardDao.insertBoard(freeBoard); //selectKey를 사용해서 boNo를 세팅해주자.
if (cnt == 0)
throw new BizNotEffectedException();
List<AttachVO> attaches = freeBoard.getAttaches();
if (attaches != null) {
for (AttachVO attach : attaches) {
attach.setAtchParentNo(freeBoard.getBoNo()); //selectKey를 사용했다면 boNo가 세팅되어있다.
attachDao.insertAttach(attach);
}
}
}
attach.setAtchParentNo(freeBoard.getBoNo()); 를 보면 getBoNo해봤자 boNo는 0이다.
글 등록시에는 파라미터로 boNo가 오지않기 때문이다.
단지 DB에서 sequence를 이용해 bo_no 칼럼에 insert한다.
그럼 글 등록시 첨부파일들한테는 어떻게 해당sequence로 insert 된 bo_no를 세팅할 수 있을까?
mybatis에서 지원하는 <selectKey> 태그를 이용하면 된다.
<selectKey>에 대한 자세한 설명은 mybatis 공식홈페이지에서 확인하고
여기서는 freeBoard.xml의 insertBoard 에서의 사용예제만 보자.
freeBoard.xml의 insertBoard
<insert id="insertBoard" parameterType="com.study.free.vo.FreeBoardVO">
<selectKey order="BEFORE" keyProperty="boNo" resultType="int">
SELECT seq_free_board.nextval FROM dual
</selectKey>
INSERT INTO free_board (
bo_no, bo_title, bo_category
, bo_writer , bo_pass, bo_content
, bo_ip, bo_hit , bo_reg_date
, bo_mod_date , bo_del_yn
) VALUES(
#{boNo}, #{boTitle} ,
#{boCategory},
#{boWriter}, #{boPass}, #{boContent},
#{boIp}, 0, sysdate,
null, 'N'
)
</insert>
이렇게 되면int cnt = freeBoardDao.insertBoard(freeBoard);
이후의 freeBoard에는 boNo가 세팅되어있다.
이를 atchParentNo에 세팅해서 첨부파일 등록 시 부모글도 같이 잘 등록된다.
이제 freeForm에서 저장버튼을 눌렀을 때 다음과 같이 첨부파일 업로드+DB에 insert가 둘다 됩니다.
첨부파일 등록된 글 보기
freeView.jsp에서 첨부파일이 있을 경우 화면에 보이게 해 봅시다.
(눌렀을 때 다운로드 되는건 나중에 하겠습니다.)
먼저 DB의 ATTACH 테이블에서 List<AttachVO>를 가져올 메소드를 만들자.
어떤글의 첨부파일들을 가지고 오는거기때문에 category, 글번호가 필요하다.
IAttachDao와 attach.xml에 메소드를 추가하자.
/** 부모번호에 따른 목록 조회 */
public List<AttachVO> getAttachListByParent(@Param(value = "atchParentNo")int atchParentNo, @Param(value = "atchCategory")String atchCategory);
<select id="getAttachListByParent" resultType="com.study.attach.vo.AttachVO" >
SELECT atch_no
, atch_original_name
, atch_fancy_size
, atch_down_hit
, TO_CHAR(atch_reg_date, 'YYYY.MM.DD') AS atch_reg_date
FROM attach
WHERE atch_parent_no = #{atchParentNo}
AND atch_category = #{atchCategory}
AND atch_del_yn = 'N'
</select>
FreeBoardServiceImpl
@Override
public FreeBoardVO getBoard(int boNo) throws BizNotFoundException {
FreeBoardVO freeBoard = freeBoardDao.getBoard(boNo);
if (freeBoard == null) {
throw new BizNotFoundException();
}
List<AttachVO> attaches = attachDao.getAttachListByParent(boNo,"FREE");
freeBoard.setAttaches(attaches);
return freeBoard;
}
이러면 객체 freeBoard에는 해당 글에 대한 List<AttachVO>가 세팅되어 있다.
이제 이 AttachVO들을 jsp화면에서 보여주기만 하면된다.
freeView.jsp 에 다음 태그를 추가하자. <tr>태그 위치에 맞게 추가하자.
<tr>
<th>첨부파일</th>
<td>
<c:forEach var="f" items="${freeBoard.attaches}" varStatus="st">
<div> 파일 ${st.count} <a href="<c:url value='/attach/download/${f.atchNo}' />" target="_blank">
<span class="glyphicon glyphicon-save" aria-hidden="true"></span> ${f.atchOriginalName}
</a> Size : ${f.atchFancySize} Down : ${f.atchDownHit}
</div>
</c:forEach>
</td>
</tr>
(첨부파일을 클릭하면 <a>태그로 요청을 하게 되는데 이 때 다운로드가 되게할 것이다. 나중에.)
다음과같이 첨부파일부분이 보인다.
여기서 잠깐.
FreeBoardServiceImpl에서 getBoard()이후 List<AttachVO>를 얻은 후에 세팅을 해줬다.
이렇게 해줘도 되지만 mybatis에서는 이 방법을 resultMap이라는 걸 통해 지원해준다.
FreeBoardVO.java
public class FreeBoardVO {
//다른필드
private List<AttachVO> attaches ; /*첨부파일 리스트 */
}
하나의 FreeBoardVO에 여러개의 AttachVO가 있는 1:n 관계이다
이 때 다음과 같이 mybatis에서 resultMap을 작성할 수 있다.
freeBoard.xml
<resultMap type="com.study.free.vo.FreeBoardVO" id="freeAttaches">
<collection property="attaches" column="{atchParentNo=bo_no, atchCategory=bo_type}"
ofType="com.study.attach.vo.AttachVO"
select="com.study.attach.dao.IAttachDao.getAttachListByParent">
</collection>
</resultMap>
<select id="getBoard" parameterType="int"
resultType="com.study.free.vo.FreeBoardVO" resultMap="freeAttaches">
SELECT
'FREE' AS bo_type,
to_char(bo_reg_date,'YYYY-MM-DD') AS bo_reg_date ,
to_char(bo_mod_date,'YYYY-MM-DD') AS bo_mod_date ,
bo_no , bo_title ,
bo_category ,
bo_writer , bo_pass , bo_content ,
bo_ip , bo_hit ,
bo_del_yn ,
b.comm_nm AS bo_category_nm
FROM free_board a
,comm_code b
WHERE bo_no=#{boNo}
AND a.bo_category=b.comm_cd
</select>
- 먼저 <select> 태그에서 resultMap 속성을 지정한다. 이는 <resultMap> 아이디와 매핑된다.
- <resultMap> 태그를 만든다. id는 resultMap의 아이디이다.
type은 내가 세팅하고 싶은 VO를 지정한다.(나는 FreeBoardVO의 List<AttachVO> attaches 속성에 값을 세팅할 것) - 1:n 관계이니까 <collection> 태그를 사용한다. 1:1관계인 경우 <association> 태그를 사용한다.
(하나의 resultMap에 <collection> <association> 이 여러개 올 수 있다.) - <collection> 태그의 select에는 실행할 쿼리 id를 쓴다. ofType에는 그 쿼리 결과타입을 쓴다.
- 참고로 mybatis에서 column=”{prop1=col1,prop2=col2}” 문법은 association 부분에 있다.
이렇게 하면 "getBoard" 메소드 실행 후 resultMap에서
지정한 쿼리 수행 후 FreeBoardVO 필드에 값이 세팅된다.
즉 FreeBoardServiceImpl에서 다음과 같이 작성하게 된다.
@Override
public FreeBoardVO getBoard(int boNo) throws BizNotFoundException {
FreeBoardVO freeBoard = freeBoardDao.getBoard(boNo);
//여기서 이미 resultMap에 의해 attaches가 세팅됨
if (freeBoard == null) {
throw new BizNotFoundException();
}
// freeBoardDao.getBoard(boNo)하는 순간 이미 attaches가 세팅됐기 때문에 필요없음
//List<AttachVO> attaches = attachDao.getAttachListByParent(boNo,"FREE");
//freeBoard.setAttaches(attaches);
return freeBoard;
}
freeEdit에서 첨부파일 수정하기
글 수정 시 원래있던 첨부파일들을 변경하거나 추가, 삭제 하게 될 것이다.
이를 위해 먼저 form태그에 enctype을 변경해주자.
<form:form action="freeModify.wow" method="post"
modelAttribute="freeBoard" enctype="multipart/form-data">
freeView처럼 현재 첨부파일에 대한 내용을 봐야하고,
freeForm처럼 첨부파일 추가,삭제 태그에 대한 이벤트를 걸어줘야한다.
formEdit.jsp에 다음과 같은 태그를 복사한다.
<tr>
<th>첨부파일
<button type="button" id="id_btn_new_file">추가</button>
</th>
<td class="file_area">
<c:forEach var="f" items="${freeBoard.attaches}" varStatus="st">
<div>
# 파일 ${st.count} <a href="<c:url value='/attach/download/${f.atchNo}' />" target="_blank"> <span class="glyphicon glyphicon-save" aria-hidden="true"></span> ${f.atchOriginalName}
</a> Size : ${f.atchFancySize} Down : ${f.atchDownHit}
<button class="btn_file_delete" data-atch-no="${f.atchNo}">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
</button>
</div>
</c:forEach>
<div class="form-inline">
<input type="file" name="boFiles" class="form-control">
<button type="button" class="btn_delete btn btn-sm">삭제</button>
</div>
</td>
</tr>
<script>
// 첨부파일 추가버튼 클릭
$('#id_btn_new_file').click(function(){
$('.file_area').append('<div class="form-inline">'
+ '<input type="file" name="boFiles" class="form-control">'
+ ' <button type="button" class="btn_delete btn btn-sm">삭제</button>'
+ '</div>');
}); // #id_btn_new_file.click
// 상위객체를 통해 이벤트 위임
$('.file_area').on('click','.btn_delete', function(){
$(this).closest('div').remove();
});
// 기존 첨부파일 삭제 클릭
$('.btn_file_delete').click(function(){
$btn = $(this);
$btn.closest('div').html(
'<input type="hidden" name="delAtchNos" value="' + $btn.data("atch-no") + '" />'
);
}); //
</script>
form에서는 DB에 등록하기 전이니까 따로 DB에서 삭제해줄 필요가 없었지만,
edit에서는 현재 DB에 있는 걸 삭제하는 경우가 필요하다.
이를 delAtchNos라는 첨부파일 번호를 파라미터로 보내서 DB에서 삭제하기로 한다.
(휴지통 버튼을 누를 때 delAtchNos라는 파라미터가 추가된다.)
FreeBoardController의 freeModify메소드
@PostMapping("/free/freeModify.wow")
public String freeModify(Model model
,@ModelAttribute("freeBoard")
@Validated(value = {Modify.class,Default.class})FreeBoardVO freeBoard
,BindingResult error
, @RequestParam(value = "boFiles",required = false)MultipartFile[] boFiles) throws IOException {
if(error.hasErrors()) {
return "free/freeEdit";
}
try {
if(boFiles!=null) {
List<AttachVO> attaches=
studyAttachUtils.getAttachListByMultiparts(boFiles, "FREE", "free");
//실제로 파일경로에 선택된 파일 올리고 List<AttachVO> return
freeBoard.setAttaches(attaches);
}
freeBoardService.modifyBoard(freeBoard);
ResultMessageVO resultMessageVO = new ResultMessageVO();
resultMessageVO.messageSetting(true, "글 수정 성공", "글을 수정했습니다.", "/free/freeList.wow", "목록으로");
model.addAttribute("resultMessageVO", resultMessageVO);
return "common/message";
} catch (BizNotFoundException enf) {
//이하 캐치문 생략
}
}
regist와 마찬가지로 추가된 첨부파일에 대해서 실제경로에 업로드 후
List<AttachVO>를 반환하고 FreeBoardVO에 setAttaches()해준다.
이제 freeBoardService.modifyBoard(freeBoard)에서 attaches를 이용해 DB에 insert해주면 된다.
그럼 삭제된 첨부파일은 어떻게 할까??
삭제를 위해 단순히 delAtchNos라는 숫자들만 파라미터로 넘어온다.
FreeBoardVO 필드에 이미 int[] delAtchNos를 추가했기 때문에 Spring이 알아서 세팅해준다.
즉, 이미 FreeBoard의 delAtchNos 필드에는 삭제할 첨부파일 번호들이 있다.
이를 가지고 freeBoardService.modifyBoard(freeBoard) 메소드를 호출한다.
FreeBoardServiceImpl의 modifyBoard
@Override
public void modifyBoard(FreeBoardVO freeBoard)
throws BizNotFoundException, BizPasswordNotMatchedException, BizNotEffectedException {
FreeBoardVO vo = freeBoardDao.getBoard(freeBoard.getBoNo());
// vo는 DB에 있는 데이터
if (vo == null)
throw new BizNotFoundException();
if (freeBoard.getBoPass().equals(vo.getBoPass())) {
int cnt = freeBoardDao.updateBoard(freeBoard);
if (cnt == 0)
throw new BizNotEffectedException();
//추가한 첨부파일 insert
List<AttachVO> attaches= freeBoard.getAttaches();
if(attaches !=null) {
for(AttachVO attach : attaches) {
attach.setAtchParentNo(freeBoard.getBoNo());
attachDao.insertAttach(attach);
}
}
// 휴지통버튼 누른 첨부파일들 DB에서 삭제
int delAtchNos[] = freeBoard.getDelAtchNos(); // 애초에 파라미터로 올때 세팅이됩니다.
if (delAtchNos != null && delAtchNos.length >0) {
attachDao.deleteAttaches(delAtchNos);
}
} else {
throw new BizPasswordNotMatchedException();
}
}
IAttachDao와 attach.xml에 deleteAttaches() 추가하기
/** 첨부파일 삭제(여러개) */
public int deleteAttaches(int[] atchNos);
<update id="deleteAttaches" parameterType="map">
UPDATE attach
SET atch_del_yn = 'Y'
WHERE 1 = 1
<!-- 파라미터가 배열일때는 parameterType=map이고 배열은 무조건 속성이 array -->
<foreach item="delAtchNo" collection="array" open="AND atch_no IN ("
close=") " separator=", ">
#{delAtchNo}
</foreach>
</update>