Spring Multipart 및 파일업로드

2022. 4. 21. 10:13Spring/Spring 실습

파일업로드 이해하기

웹에서는 이 클라이언트/서버 간 요청/응답을 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>