Spring MVC + File Upload
BoardController.java (Controller)
package com.gd.article.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.gd.article.dto.BoardArticle;
import com.gd.article.dto.BoardRequest;
import com.gd.article.service.BoardService;
import com.gd.article.util.TeamColor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
public class BoardController {
@Autowired
BoardService boardService;
@GetMapping("/addBoard")
public String addBoard() {
return "addBoard";
}
@PostMapping("/addBoard")
/*
-- 커맨드 객체를 사용하지 않고 코드를 풀어 쓴다면 --
@PostMapping("/addBoard")
public String addBoard(
@RequestParam(name="boardTitle") String boardTitle,
@RequestParam(name="boardContent") String boardContent,
MultipartFile boardFile
)
>> File 업로드 변수인 MultipartFile의 경우
@RequestParam(name="")을 사용하지 않는 것을 확인할 수 있음
그럼 MultipartFile 타입의 경우 @RequestParam을 사용할 수 없는 것인가?
하면 그것도 아닌거 같음
*/
public String addBoard(BoardRequest br) {
log.debug(TeamColor.YELLOW + "boardTitle : " + br.getBoardTitle());
log.debug(TeamColor.YELLOW + "boardContent : " + br.getBoardContent());
log.debug(TeamColor.YELLOW + "boardFile.originalName : " + br.getBoardFile().getOriginalFilename());
boardService.addBoard(br);
return "redirect:/boardList";
}
@GetMapping("/boardList")
public String boardList(Model model,
@RequestParam(name="currentPage", defaultValue = "1") int currentPage,
@RequestParam(name="rowPerPage", defaultValue = "5") int rowPerPage) {
List<String> list = boardService.selectBoardList(currentPage, rowPerPage);
int lastPage = boardService.getBoardCount() / rowPerPage;
if(lastPage % rowPerPage != 0) {
lastPage++;
}
model.addAttribute("list", list);
model.addAttribute("currentPage", currentPage);
model.addAttribute("rowPerPage", rowPerPage);
model.addAttribute("lastPage", lastPage);
return "boardList";
}
@GetMapping("/deleteBoard")
public String deleteBoard(int articleNo) {
boardService.deleteBoard(articleNo);
return "redirect:/boardList";
}
}
BoardService.java (Service)
package com.gd.article.service;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.gd.article.dto.BoardArticle;
import com.gd.article.dto.BoardFile;
import com.gd.article.dto.BoardRequest;
import com.gd.article.mapper.BoardArticleMapper;
import com.gd.article.mapper.BoardFileMapper;
import com.gd.article.util.TeamColor;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
@Transactional
public class BoardService {
@Autowired
BoardArticleMapper boardArticleMapper;
@Autowired
BoardFileMapper boardFileMapper;
public void addBoard(BoardRequest br) {
// boardArticle 등록
BoardArticle ba = new BoardArticle();
ba.setArticleTitle(br.getBoardTitle());
ba.setArticleContent(br.getBoardContent());
int addArticleRow = boardArticleMapper.insertBoardArticle(ba);
log.debug(TeamColor.YELLOW + "after insert articleNo : " + ba.getArticleNo());
// article 등록에 실패 했을경우 트랜잭션
if(addArticleRow != 1) {
throw new RuntimeException();
}
// boardFile 등록
MultipartFile mf = br.getBoardFile();
BoardFile bf = new BoardFile();
bf.setArticleNo(ba.getArticleNo());
bf.setOriginalName(br.getBoardFile().getOriginalFilename());
bf.setFileType(mf.getContentType());
bf.setFileSize(mf.getSize());
// 저장될 파일 이름은 UUID 사용
String prefix = UUID.randomUUID().toString().replace("-", "");
int p = mf.getOriginalFilename().lastIndexOf(".");
String suffix = mf.getOriginalFilename().substring(p);
bf.setFileName(prefix + suffix);
// Mapper 호출
int addFileRow = boardFileMapper.insertBoardFile(bf);
// file 등록에 실패했을 경우 트랜잭션
if(addFileRow != 1) {
throw new RuntimeException();
}
// 파일 저장
// MultipartFile mf 의 파일(스트림)을 비어있는 emptyFile로 복사
File emptyFile = new File("C:\\upload\\" + prefix + suffix);
try {
mf.transferTo(emptyFile);
} catch (Exception e) {
e.printStackTrace();
// 위 코드에서 예외가 발생할 경우 프로그램이 죽어버림
// try catch 를 사용하지 않지만 에러(예외)가 발생한 경우 transactional 을 발동 시키기 위해 예외 발생시키기
throw new RuntimeException();
}
}
public List<String> selectBoardList(int currentPage, int rowPerPage) {
int startRow = (currentPage - 1) * rowPerPage;
Map<String, Integer> map = new HashMap<>();
map.put("startRow", startRow);
map.put("rowPerPage", rowPerPage);
List<String> list = boardArticleMapper.selectBoardList(map);
log.debug("list : " + list.toString());
return list;
}
public int getBoardCount() {
int boardCount = boardArticleMapper.getBoardCount();
return boardCount;
}
public void deleteBoard(int articleNo) {
int deleteArticleRow = boardArticleMapper.deleteBoardArticle(articleNo);
log.debug("deleteArticleRow : " + deleteArticleRow);
if(deleteArticleRow != 1) {
throw new RuntimeException();
}
int deleteFileRow = boardFileMapper.deleteBoardFile(articleNo);
log.debug("deleteFileRow : " + deleteFileRow);
if(deleteFileRow != 1) {
throw new RuntimeException();
}
}
}
BoardArticle.java (DTO)
package com.gd.article.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 커맨드 객체 생성
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BoardArticle {
private int articleNo;
private String articleTitle;
private String articleContent;
private String updateDate;
private String createDate;
// BoardFile 의 이미지를 가져오기 위해 추가
private String fileName;
}
BoardFile.java (DTO)
package com.gd.article.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BoardFile {
private int fileNo;
private int articleNo;
private String fileName;
private String originalName;
private String fileType;
private long fileSize;
private String updateDate;
private String createDate;
}
BoardRequest.java (DTO - Insert Transaction)
package com.gd.article.dto;
import org.springframework.web.multipart.MultipartFile;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BoardRequest {
private String boardTitle;
private String boardContent;
private MultipartFile boardFile;
}
BoardArticleMapper.java (Mapper)
package com.gd.article.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import com.gd.article.dto.BoardArticle;
@Mapper
public interface BoardArticleMapper {
int insertBoardArticle(BoardArticle ba);
List<String> selectBoardList(Map<String, Integer> map);
int getBoardCount();
int deleteBoardArticle(int articleNo);
}
BoardArticleMapper.xml (Mapper.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.gd.article.mapper.BoardArticleMapper">
<insert id="insertBoardArticle" parameterType="com.gd.article.dto.BoardArticle">
<selectKey resultType="int" keyProperty="articleNo" order="AFTER" >
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO board_article (
article_title,
article_content,
update_date,
create_date
) VALUES (
#{articleTitle},
#{articleContent},
NOW(),
NOW()
)
</insert>
<select id="selectBoardList" resultType="com.gd.article.dto.BoardArticle" parameterType="Map">
SELECT
a.article_no articleNo,
a.article_title articleTitle,
a.article_content articleContent,
b.file_name fileName,
a.update_date updateDate,
a.create_date createDate
FROM
board_article a
INNER JOIN
board_file b
ON
a.article_no = b.article_no
LIMIT
#{startRow}, #{rowPerPage}
</select>
<delete id="deleteBoardArticle" parameterType="int">
DELETE FROM
board_article
WHERE
article_no = #{articleNo}
</delete>
<select id="getBoardCount" resultType="int">
SELECT
COUNT(*)
FROM
board_article
</select>
</mapper>
BoardFileMapper.java (Mapper)
package com.gd.article.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.gd.article.dto.BoardFile;
@Mapper
public interface BoardFileMapper {
int insertBoardFile(BoardFile bf);
int deleteBoardFile(int articleNo);
}
BoardFileMapper.xml (Mapper.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.gd.article.mapper.BoardFileMapper">
<insert id="insertBoardFile" parameterType="com.gd.article.dto.BoardFile">
INSERT INTO board_file (
article_no,
file_name,
original_name,
file_type,
file_size,
update_date,
create_date
) VALUES (
#{articleNo},
#{fileName},
#{originalName},
#{fileType},
#{fileSize},
NOW(),
NOW()
)
</insert>
<delete id="deleteBoardFile" parameterType="int">
DELETE FROM
board_file
WHERE
article_no = #{articleNo}
</delete>
</mapper>
boardList.jsp (View)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Board List</title>
<style>
td{
text-align: center;
vertical-align: middle;
}
h1{
text-align: center;
padding-top: 30px;
}
.btn{text-align: center;}
</style>
<!-- Latest compiled and minified CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Latest compiled JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div class="container">
<h1>Board List</h1>
<br>
<table class="table table-bordered table-striped">
<tr>
<td>Article No</td>
<td>Article Title</td>
<td>Article Content</td>
<td>Article Image</td>
<td>Update Date</td>
<td>Create Date</td>
<td> </td>
</tr>
<c:forEach var="a" items="${list}">
<tr>
<td>${a.articleNo}</td>
<td>${a.articleTitle}</td>
<td>${a.articleContent}</td>
<td><img src="/article/img/${a.fileName}" width="100px" height="100px"></td>
<td>${a.updateDate}</td>
<td>${a.createDate}</td>
<td><a class="btn btn-danger"href="${pageContext.request.contextPath}/deleteBoard?articleNo=${a.articleNo}">삭제하기</a></td>
</tr>
</c:forEach>
</table>
<br>
<a class="btn btn-primary" href="${pageContext.request.contextPath}/addBoard">등록하기</a>
<c:if test="${currentPage > 1}">
<a class="btn btn-dark" href="${pageContext.request.contextPath}/boardList?currentPage=${currentPage-1}&rowPerPage=${rowPerPage}">이전</a>
</c:if>
<c:if test="${currentPage < lastPage}">
<a class="btn btn-dark" href="${pageContext.request.contextPath}/boardList?currentPage=${currentPage+1}&rowPerPage=${rowPerPage}">다음</a>
</c:if>
</div>
</body>
</html>
boardList.jsp 출력결과
