27. 스프링 게시판 만들기 / 첨부파일 업로드, 다운로드(4)
안녕하세요
이전 포스팅에서 파일 다운로드까지 진행했었는데요.
이번에는 다중 첨부파일과 첨부파일 수정, 삭제를 만들어보겠습니다.
1. boardMapper.xml 작성
글 작성 페이지에서 파일을 등록하거나 글 수정 페이지에서 파일을 수정 또는 삭제할때 파일이
등록된 순서대로 조회할 수 있도록 ORDER BY FILE_NO ASC를 추가해줍니다.
그리고 조건에 AND DEL_GN = 'N'이 추가되었습니다.
UPDATE쿼리를 이용하여 삭제한 파일은 DEL_GB를 Y로 바꿉니다.
여기서 파일을 실제로 삭제하는것이 아니라 DEL_GB에 따라 N값이면 보여주고 Y이면 보여주지 않는 식으로 진행하려고 합니다.
<?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="boardMapper">
<!-- 게시판 글 작성 -->
<insert id="insert" parameterType="kr.co.vo.BoardVO" useGeneratedKeys="true" keyProperty="bno">
<selectKey keyProperty="bno" resultType="int" order="BEFORE">
SELECT MP_BOARD_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO MP_BOARD( BNO
, TITLE
, CONTENT
, WRITER )
VALUES( #{bno}
, #{title}
, #{content}
, #{writer} )
</insert>
<select id="listPage" resultType="kr.co.vo.BoardVO" parameterType="kr.co.vo.SearchCriteria">
SELECT BNO,
TITLE,
CONTENT,
WRITER,
REGDATE
FROM (
SELECT BNO,
TITLE,
CONTENT,
WRITER,
REGDATE,
ROW_NUMBER() OVER(ORDER BY BNO DESC) AS RNUM
FROM MP_BOARD
WHERE 1=1
<include refid="search"></include>
) MP
WHERE RNUM BETWEEN #{rowStart} AND #{rowEnd}
ORDER BY BNO DESC
</select>
<select id="listCount" parameterType="kr.co.vo.SearchCriteria" resultType="int">
SELECT COUNT(BNO)
FROM MP_BOARD
WHERE 1=1
<include refid="search"></include>
AND BNO > 0
</select>
<sql id="search">
<if test="searchType != null">
<if test="searchType == 't'.toString()">AND TITLE LIKE '%' || #{keyword} || '%'</if>
<if test="searchType == 'c'.toString()">AND CONTENT LIKE '%' || #{keyword} || '%'</if>
<if test="searchType == 'w'.toString()">AND WRITER LIKE '%' || #{keyword} || '%'</if>
<if test="searchType == 'tc'.toString()">AND (TITLE LIKE '%' || #{keyword} || '%') or (CONTENT LIKE '%' || #{keyword} || '%')</if>
</if>
</sql>
<select id="read" parameterType="int" resultType="kr.co.vo.BoardVO">
SELECT BNO
, TITLE
, CONTENT
, WRITER
, REGDATE
FROM MP_BOARD
WHERE BNO = #{bno}
</select>
<update id="update" parameterType="kr.co.vo.BoardVO">
UPDATE MP_BOARD
SET TITLE = #{title},
CONTENT = #{content}
WHERE BNO = #{bno}
</update>
<delete id="delete" parameterType="int">
DELETE
FROM MP_BOARD
WHERE BNO = #{bno}
</delete>
<!-- 첨부파일 업로드 -->
<insert id="insertFile" parameterType="hashMap">
INSERT INTO MP_FILE(
FILE_NO,
BNO,
ORG_FILE_NAME,
STORED_FILE_NAME,
FILE_SIZE
)VALUES(
SEQ_MP_FILE_NO.NEXTVAL,
#{BNO},
#{ORG_FILE_NAME},
#{STORED_FILE_NAME},
#{FILE_SIZE}
)
</insert>
<!-- 첨부파일 조회 -->
<select id="selectFileList" parameterType="int" resultType="hashMap">
SELECT FILE_NO,
ORG_FILE_NAME,
ROUND(FILE_SIZE/1024,1) AS FILE_SIZE,
DEL_GB
FROM MP_FILE
WHERE BNO = #{BNO}
AND DEL_GB = 'N'
ORDER BY FILE_NO ASC
</select>
<!-- 첨부파일 다운 -->
<select id="selectFileInfo" parameterType="hashMap" resultType="hashMap">
SELECT
STORED_FILE_NAME,
ORG_FILE_NAME
FROM MP_FILE
WHERE FILE_NO = #{FILE_NO}
</select>
<update id="updateFile" parameterType="hashMap">
UPDATE MP_FILE SET
DEL_GB = 'Y'
WHERE FILE_NO = #{FILE_NO}
</update>
</mapper>
2. BoardDAO작성
Map형태의 파라미터(map)를 받아 보내줍니다.
map는 FILE_NO가 있겠지요.
BoardDAO.java
// 첨부파일 수정
public void updateFile(Map<String, Object> map) throws Exception;
BoardDAOImpl.java
@Override
public void updateFile(Map<String, Object> map) throws Exception {
// TODO Auto-generated method stub
sqlSession.update("boardMapper.updateFile", map);
}
3. BoardService작성
게시물 수정 메소드에서 파라미터들을 추가해줍니다.
BoardServiceImpl.java를 보시면
64번줄은 게시물을 업데이트하는 쿼리이고
66번줄 : 파일 업데이트할 값들을 list에 담습니다.
69번줄 : fileUtils.parseUpdateFileInfo()결과의 size만큼 for문을 돌립니다.
70~75번줄 : tempMap에 list.get(i)를 담고 if문을 이용하여 tempMap에서 IS_NEW를 꺼내와서 값이Y와 같으면
dao.insertFile(tempMap)를 실행합니다. 같지 않으면 dao.updateFile(tempMap)실행합니다.
BoardService.java
// 게시물 수정
public void update(BoardVO boardVO, String[] files, String[] fileNames, MultipartHttpServletRequest mpRequest) throws Exception;
BoardServiceImpl.java
@Override
public void update(BoardVO boardVO, String[] files, String[] fileNames, MultipartHttpServletRequest mpRequest) throws Exception {
dao.update(boardVO);
List<Map<String, Object>> list = fileUtils.parseUpdateFileInfo(boardVO, files, fileNames, mpRequest);
Map<String, Object> tempMap = null;
int size = list.size();
for(int i = 0; i<size; i++) {
tempMap = list.get(i);
if(tempMap.get("IS_NEW").equals("Y")) {
dao.insertFile(tempMap);
}else {
dao.updateFile(tempMap);
}
}
}
4. FileUtils작성
parseUpdateFileInfo메소드를 작성해줍니다.
74번줄 : if(multipartFile.isEmpty() == false)
multipartFile이 비어있지 않으면 if문을 타게되는데
새로운 새로운 첨부파일이 등록되었을때 타게됩니다.
88번줄 : files와 fileNames가 null이 아니면 for문을 타게됩니다.
이 for문은 삭제할 파일의 파일 번호와 파일이름을 받게되어있습니다.
package kr.co.util;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import kr.co.vo.BoardVO;
@Component("fileUtils")
public class FileUtils {
private static final String filePath = "C:\\mp\\file\\"; // 파일이 저장될 위치
public List<Map<String, Object>> parseInsertFileInfo(BoardVO boardVO,
MultipartHttpServletRequest mpRequest) throws Exception{
/*
Iterator은 데이터들의 집합체? 에서 컬렉션으로부터 정보를 얻어올 수 있는 인터페이스입니다.
List나 배열은 순차적으로 데이터의 접근이 가능하지만, Map등의 클래스들은 순차적으로 접근할 수가 없습니다.
Iterator을 이용하여 Map에 있는 데이터들을 while문을 이용하여 순차적으로 접근합니다.
*/
Iterator<String> iterator = mpRequest.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;
int bno = boardVO.getBno();
File file = new File(filePath);
if(file.exists() == false) {
file.mkdirs();
}
while(iterator.hasNext()) {
multipartFile = mpRequest.getFile(iterator.next());
if(multipartFile.isEmpty() == false) {
originalFileName = multipartFile.getOriginalFilename();
originalFileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
storedFileName = getRandomString() + originalFileExtension;
file = new File(filePath + storedFileName);
multipartFile.transferTo(file);
listMap = new HashMap<String, Object>();
listMap.put("BNO", bno);
listMap.put("ORG_FILE_NAME", originalFileName);
listMap.put("STORED_FILE_NAME", storedFileName);
listMap.put("FILE_SIZE", multipartFile.getSize());
list.add(listMap);
}
}
return list;
}
public List<Map<String, Object>> parseUpdateFileInfo(BoardVO boardVO, String[] files, String[] fileNames, MultipartHttpServletRequest mpRequest) throws Exception{
Iterator<String> iterator = mpRequest.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;
int bno = boardVO.getBno();
while(iterator.hasNext()){
multipartFile = mpRequest.getFile(iterator.next());
if(multipartFile.isEmpty() == false){
originalFileName = multipartFile.getOriginalFilename();
originalFileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
storedFileName = getRandomString() + originalFileExtension;
multipartFile.transferTo(new File(filePath + storedFileName));
listMap = new HashMap<String,Object>();
listMap.put("IS_NEW", "Y");
listMap.put("BNO", bno);
listMap.put("ORG_FILE_NAME", originalFileName);
listMap.put("STORED_FILE_NAME", storedFileName);
listMap.put("FILE_SIZE", multipartFile.getSize());
list.add(listMap);
}
}
if(files != null && fileNames != null){
for(int i = 0; i<fileNames.length; i++) {
listMap = new HashMap<String,Object>();
listMap.put("IS_NEW", "N");
listMap.put("FILE_NO", files[i]);
list.add(listMap);
}
}
return list;
}
public static String getRandomString() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
5. BoardController작성
98,99번줄 : updateView페이지에 파일리스트가 보이게 추가해줍니다.
105번줄 : 파라미터에 @RequestParam이 붙은 fileNoDel[]과 fileNameDel[]은 jsp에서 fileNoDel[]과 fileNameDel[]로 지정한 값을 String[] 타입으로 담겠다는 말입니다.
그럼 이 값들을 112번줄 메소드에 파라미터값이 전달되도록 수정해줍니다.
// 게시판 수정뷰
@RequestMapping(value = "/updateView", method = RequestMethod.GET)
public String updateView(BoardVO boardVO, @ModelAttribute("scri") SearchCriteria scri, Model model)
throws Exception {
logger.info("updateView");
model.addAttribute("update", service.read(boardVO.getBno()));
model.addAttribute("scri", scri);
List<Map<String, Object>> fileList = service.selectFileList(boardVO.getBno());
model.addAttribute("file", fileList);
return "board/updateView";
}
// 게시판 수정
@RequestMapping(value = "/update", method = RequestMethod.POST)
public String update(BoardVO boardVO,
@ModelAttribute("scri") SearchCriteria scri,
RedirectAttributes rttr,
@RequestParam(value="fileNoDel[]") String[] files,
@RequestParam(value="fileNameDel[]") String[] fileNames,
MultipartHttpServletRequest mpRequest) throws Exception {
logger.info("update");
service.update(boardVO, files, fileNames, mpRequest);
rttr.addAttribute("page", scri.getPage());
rttr.addAttribute("perPageNum", scri.getPerPageNum());
rttr.addAttribute("searchType", scri.getSearchType());
rttr.addAttribute("keyword", scri.getKeyword());
return "redirect:/board/list";
}
6. writeView작성
writeView를 조금 수정해줬습니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<!-- 합쳐지고 최소화된 최신 CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<!-- 부가적인 테마 -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<title>게시판</title>
</head>
<script type="text/javascript">
$(document).ready(function(){
var formObj = $("form[name='writeForm']");
$(".write_btn").on("click", function(){
if(fn_valiChk()){
return false;
}
formObj.attr("action", "/board/write");
formObj.attr("method", "post");
formObj.submit();
});
fn_addFile();
})
function fn_valiChk(){
var regForm = $("form[name='writeForm'] .chk").length;
for(var i = 0; i<regForm; i++){
if($(".chk").eq(i).val() == "" || $(".chk").eq(i).val() == null){
alert($(".chk").eq(i).attr("title"));
return true;
}
}
}
function fn_addFile(){
var fileIndex = 1;
//$("#fileIndex").append("<div><input type='file' style='float:left;' name='file_"+(fileIndex++)+"'>"+"<button type='button' style='float:right;' id='fileAddBtn'>"+"추가"+"</button></div>");
$(".fileAdd_btn").on("click", function(){
$("#fileIndex").append("<div><input type='file' style='float:left;' name='file_"+(fileIndex++)+"'>"+"</button>"+"<button type='button' style='float:right;' id='fileDelBtn'>"+"삭제"+"</button></div>");
});
$(document).on("click","#fileDelBtn", function(){
$(this).parent().remove();
});
}
</script>
<body>
<div id="root">
<header>
<h1> 게시판</h1>
</header>
<hr />
<div>
<%@include file="nav.jsp" %>
</div>
<hr />
<section id="container">
<form name="writeForm" method="post" action="/board/write" enctype="multipart/form-data">
<table>
<tbody>
<c:if test="${member.userId != null}">
<tr>
<td>
<label for="title">제목</label><input type="text" id="title" name="title" class="chk" title="제목을 입력하세요."/>
</td>
</tr>
<tr>
<td>
<label for="content">내용</label><textarea id="content" name="content" class="chk" title="내용을 입력하세요."></textarea>
</td>
</tr>
<tr>
<td>
<label for="writer">작성자</label><input type="text" id="writer" name="writer" class="chk" title="작성자를 입력하세요." value="${member.userId}" />
</td>
</tr>
<tr>
<tr>
<td id="fileIndex">
</td>
</tr>
<tr>
<td>
<button class="write_btn" type="submit">작성</button>
<button class="fileAdd_btn" type="button">파일추가</button>
</td>
</tr>
</c:if>
<c:if test="${member.userId == null}">
<p>로그인 후에 작성하실 수 있습니다.</p>
</c:if>
</tbody>
</table>
</form>
</section>
<hr />
</div>
</body>
</html>
7. updateView작성
삭제버튼을 눌렀을때 파일의 번호와 파일의 인덱스를 파라미터로 fn_del()함수에 보내줍니다.
push를 이용하여 삭제한 번호를 담아줍니다.
담은 파일을 94, 95번줄 input태그에 값을 담아줍니다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<html>
<head>
<!-- 합쳐지고 최소화된 최신 CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<!-- 부가적인 테마 -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<title>게시판</title>
</head>
<script type="text/javascript">
$(document).ready(function(){
var formObj = $("form[name='updateForm']");
$(document).on("click","#fileDel", function(){
$(this).parent().remove();
})
fn_addFile();
$(".cancel_btn").on("click", function(){
event.preventDefault();
location.href = "/board/readView?bno=${update.bno}"
+ "&page=${scri.page}"
+ "&perPageNum=${scri.perPageNum}"
+ "&searchType=${scri.searchType}"
+ "&keyword=${scri.keyword}";
})
$(".update_btn").on("click", function(){
if(fn_valiChk()){
return false;
}
formObj.attr("action", "/board/update");
formObj.attr("method", "post");
formObj.submit();
})
})
function fn_valiChk(){
var updateForm = $("form[name='updateForm'] .chk").length;
for(var i = 0; i<updateForm; i++){
if($(".chk").eq(i).val() == "" || $(".chk").eq(i).val() == null){
alert($(".chk").eq(i).attr("title"));
return true;
}
}
}
function fn_addFile(){
var fileIndex = 1;
//$("#fileIndex").append("<div><input type='file' style='float:left;' name='file_"+(fileIndex++)+"'>"+"<button type='button' style='float:right;' id='fileAddBtn'>"+"추가"+"</button></div>");
$(".fileAdd_btn").on("click", function(){
$("#fileIndex").append("<div><input type='file' style='float:left;' name='file_"+(fileIndex++)+"'>"+"</button>"+"<button type='button' style='float:right;' id='fileDelBtn'>"+"삭제"+"</button></div>");
});
$(document).on("click","#fileDelBtn", function(){
$(this).parent().remove();
});
}
var fileNoArry = new Array();
var fileNameArry = new Array();
function fn_del(value, name){
fileNoArry.push(value);
fileNameArry.push(name);
$("#fileNoDel").attr("value", fileNoArry);
$("#fileNameDel").attr("value", fileNameArry);
}
</script>
<body>
<div id="root">
<header>
<h1> 게시판</h1>
</header>
<hr />
<div>
<%@include file="nav.jsp" %>
</div>
<hr />
<section id="container">
<form name="updateForm" role="form" method="post" action="/board/update" enctype="multipart/form-data">
<input type="hidden" name="bno" value="${update.bno}" readonly="readonly"/>
<input type="hidden" id="page" name="page" value="${scri.page}">
<input type="hidden" id="perPageNum" name="perPageNum" value="${scri.perPageNum}">
<input type="hidden" id="searchType" name="searchType" value="${scri.searchType}">
<input type="hidden" id="keyword" name="keyword" value="${scri.keyword}">
<input type="hidden" id="fileNoDel" name="fileNoDel[]" value="">
<input type="hidden" id="fileNameDel" name="fileNameDel[]" value="">
<table>
<tbody>
<tr>
<td>
<label for="title">제목</label><input type="text" id="title" name="title" value="${update.title}" class="chk" title="제목을 입력하세요."/>
</td>
</tr>
<tr>
<td>
<label for="content">내용</label><textarea id="content" name="content" class="chk" title="내용을 입력하세요."><c:out value="${update.content}" /></textarea>
</td>
</tr>
<tr>
<td>
<label for="writer">작성자</label><input type="text" id="writer" name="writer" value="${update.writer}" readonly="readonly"/>
</td>
</tr>
<tr>
<td>
<label for="regdate">작성날짜</label>
<fmt:formatDate value="${update.regdate}" pattern="yyyy-MM-dd"/>
</td>
</tr>
<tr>
<td id="fileIndex">
<c:forEach var="file" items="${file}" varStatus="var">
<div>
<input type="hidden" id="FILE_NO" name="FILE_NO_${var.index}" value="${file.FILE_NO }">
<input type="hidden" id="FILE_NAME" name="FILE_NAME" value="FILE_NO_${var.index}">
<a href="#" id="fileName" onclick="return false;">${file.ORG_FILE_NAME}</a>(${file.FILE_SIZE}kb)
<button id="fileDel" onclick="fn_del('${file.FILE_NO}','FILE_NO_${var.index}');" type="button">삭제</button><br>
</div>
</c:forEach>
</td>
</tr>
</tbody>
</table>
<div>
<button type="button" class="update_btn">저장</button>
<button type="button" class="cancel_btn">취소</button>
<button type="button" class="fileAdd_btn">파일추가</button>
</div>
</form>
</section>
<hr />
</div>
</body>
</html>
8. 실행테스트
프로젝트 첨부파일 입니다.