게시판 기능에 파일업로드도 추가해보도록 한다.
@Autowired
FileService fileService;
private static String UPLOAD_FOLDER = "D:/logs/";
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
@PostMapping("/Modify")
public ModelAndView modifyPost(HttpServletRequest request, HttpServletResponse response, @RequestParam("file") MultipartFile file) throws UnsupportedEncodingException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
request.setCharacterEncoding("UTF-8");
String num = request.getParameter("num");
Post post = postService.getPost(num);
post.setAuthor(request.getParameter("author"));
post.setContents(request.getParameter("contents"));
post.setTitle(request.getParameter("title"));
post.setNum(request.getParameter("num"));
if (!file.isEmpty()){
try{
FileInfo fileInfo = new FileInfo();
String originalFilename = file.getOriginalFilename();
// String fileExt = FilenameUtils.getExtension(file.getOriginalFilename());
String fileExt = originalFilename.contains(".")
? originalFilename.substring(originalFilename.lastIndexOf('.') + 1)
: "";
//랜덤 이름 생성 (겹치지 않기 위해서)
String storedFilename = UUID.randomUUID().toString() + "." + fileExt;
Path path = Paths.get(UPLOAD_FOLDER, storedFilename);
byte[] bytes = file.getBytes();
//실제 파일 저장
Files.write(path, bytes);
fileInfo.setOriginalFilename(originalFilename);
fileInfo.setStoredFilename(storedFilename);
fileInfo.setFileExt(fileExt);
fileInfo.setFilePath(UPLOAD_FOLDER);
fileInfo.setPostId(post.getNum());
// fileService.insertFile(fileInfo);
postService.modifyPost(post, fileInfo);
}catch (IOException e){
e.printStackTrace();
}
}else{
postService.modifyPost(post);
}
mav.setViewName("redirect:/Content?num=" + post.getNum());
return mav;
}
@PostMapping("/Write")
public ModelAndView writePost(@ModelAttribute Post post, HttpServletResponse response, @RequestParam("file") MultipartFile file) throws UnsupportedEncodingException{
System.out.println("test");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
System.out.println(file + "dd " + !file.isEmpty());
if (!file.isEmpty()){
try{
FileInfo fileInfo = new FileInfo();
String originalFilename = file.getOriginalFilename();
String fileExt = originalFilename.contains(".")
? originalFilename.substring(originalFilename.lastIndexOf('.') + 1)
: "";
//랜덤 이름 생성 (겹치지 않기 위해서)
String storedFilename = UUID.randomUUID().toString() + "." + fileExt;
Path path = Paths.get(UPLOAD_FOLDER, storedFilename);
byte[] bytes = file.getBytes();
//실제 파일 저장
Files.write(path, bytes);
fileInfo.setOriginalFilename(originalFilename);
fileInfo.setStoredFilename(storedFilename);
fileInfo.setFileExt(fileExt);
fileInfo.setFilePath(UPLOAD_FOLDER);
postService.inserPost(post, fileInfo);
}catch (IOException e){
e.printStackTrace();
}
}else{
postService.inserPost(post);
}
mav.setViewName("redirect:/");
return mav;
}
새롭게 추가한 부분은 if 문 file.isEmpty() 부분이다.
파일이 있으면 파일 업로드 로직을 수행하게 된다.
상단에 static 변수와 FileService 도 주입시켜주었다.
File VO(model) 객체도 생성하였는데
package com.example.post.model;
import lombok.Data;
@Data
public class FileInfo {
private String fileId;
private String postId;
private String originalFilename;
private String storedFilename;
private String filePath;
private String fileExt;
}
fileId, postId(게시글), original(업로드 시 파일이름), stored(저장시 파일이름), 파일경로, Ext(확장자) 이다.
위 메서드에 보면 storedFileName 을 무작위로 생성하여, fileExt(확장자)와 더해 저장하게 되는데
무작위로 생성하여 굳이 저장시 파일 이름을 만드는 이유는 업로드 파일이름이 겹칠 수 있어 다운로드때 문제가 발생할 수 있기 때문이다.
CREATE TABLE FileInfo (
fileId INT IDENTITY(1,1) PRIMARY KEY,
postId INT NOT NULL,
originalFilename NVARCHAR(255) NOT NULL,
storedFilename NVARCHAR(255) NOT NULL,
filePath NVARCHAR(500) NOT NULL,
fileExt NVARCHAR(10),
FOREIGN KEY (postId) REFERENCES test1(num)
);
테이블 생성은 이렇게 했다.
다운로드 컨트롤러는 내가 따로만들었는데.. 파일컨트롤러로 구분하는게 더 좋은지 아직 방법은 잘 모르겠다.
controller/DownloadController.java
package com.example.post.controller;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.post.model.FileInfo;
import com.example.post.service.FileService;
@Controller
@RequestMapping("/Download")
public class DownloadController {
private static String UPLOAD_FOLDER = "D:/logs/";
@Autowired FileService fileService;
@GetMapping("/{fileId}")
public ResponseEntity<Resource> downloadFile(@PathVariable("fileId") int fileId) throws MalformedURLException, UnsupportedEncodingException {
FileInfo fileInfo = fileService.selectFileInfo(fileId);
if(fileInfo == null){
return ResponseEntity.notFound().build();
}
Path path = Paths.get(fileInfo.getFilePath(), fileInfo.getStoredFilename());
Resource resource = new UrlResource(path.toUri());
if(!resource.exists()){
return ResponseEntity.notFound().build();
}
// 파일명을 UTF-8로 인코딩
String encodedFileName = URLEncoder.encode(fileInfo.getOriginalFilename(), StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
String contentDisposition = "attachment; filename*=UTF-8''" + encodedFileName;
//응답헤더에 CONTETN_DISPOSITION -> 첨부파일이라고 알려줌, attachment; 다음나오는것 다운로드, .body(resource)->resource 에 파일내용 담아감
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
}
컨트롤러를 다 만들었으니 Service 수정을 해본다.
우선 업로드시에 파일도 추가하여야 하므로 PostService 부터 수정한다.
@Autowired FileDao fileDao;
@Autowired FileService fileService;
@Transactional
public void inserPost(Post post, FileInfo fileInfo) {
postDao.insertPost(post);
fileInfo.setPostId(post.getNum());
fileDao.insertFile(fileInfo);
}
Transactional 처리를 해준다.
@Transactional
public void deletePost(String num) throws IOException {
commentDao.deleteCommentByPostId(num);
List<FileInfo> fileInfoList = fileDao.selectFileByPostId(num);
if(!fileInfoList.isEmpty()){
for(FileInfo fileInfo : fileInfoList){
System.out.println(fileInfo + " \n\n\n\n");
fileService.deleteFile(fileInfo);
}
}
//외래키때문에 맨마지막에 삭제해야함.
postDao.deletePost(num);
}
@Transactional
public void modifyPost(Post post, FileInfo fileInfo) throws IOException {
postDao.modifyPost(post);
String fileId = fileDao.selectFileByPostId(post.getNum()).get(0).getFileId();
System.out.println(fileId);
fileInfo.setFileId(fileId);
fileService.deleteFile(fileInfo);
fileDao.insertFile(fileInfo);
}
그외에 수정, 삭제에도 추가해준다.
외래키가 설정되어있으므로, 파일부터 지우고 게시글을 지울수 있도록 한다.
modify 의 경우 파일id 를 얻어온 뒤 로직 수행
service package 에 FileService.java 도 생성해준다.
package com.example.post.service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.post.dao.FileDao;
import com.example.post.model.FileInfo;
@Service
public class FileService {
@Autowired
FileDao fileDao;
public void insertFile(FileInfo fileInfo) {
fileDao.insertFile(fileInfo);
}
public FileInfo selectFileInfo(int fileId) {
return fileDao.selectFile(fileId);
}
public List<FileInfo> selectFileByPostId(String num) {
return fileDao.selectFileByPostId(num);
}
public void deleteFile(FileInfo fileInfo) throws IOException{
Files.delete(Paths.get(fileInfo.getFilePath() + "" + fileInfo.getStoredFilename()));
fileDao.deleteFile(fileInfo.getFileId());
}
}
delete 의 경우 실제 파일도 지워야하므로, Files.delete(path) 를 통해 실제 경로 파일을 삭제하고, DB 경로도 지운다.
이제보니 Files.delete 결과를 제대로 받고나서 Dao 수행하는게 맞는거같다.
dao package 에도 Dao 를 하나더 만들어준다.
FileDao.java
package com.example.post.dao;
import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.example.post.model.FileInfo;
@Repository
public class FileDao {
@Autowired
private SqlSessionTemplate sqlSession;
public void insertFile(FileInfo fileInfo) {
sqlSession.insert("FileMapper.insertFile", fileInfo);
}
public FileInfo selectFile(int fileId) {
return sqlSession.selectOne("FileMapper.selectFile", fileId);
}
public List<FileInfo> selectFileByPostId(String num) {
return sqlSession.selectList("FileMapper.selectFileByPostId", Integer.parseInt(num));
}
public void deleteFile(String fileId) {
sqlSession.delete("FileMapper.deleteFile", Integer.parseInt(fileId));
}
}
새로운 mapper 를 작성할것이기에 FileMapper.~~~ 로 지정하였다.
아래는 mmapper-file.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="FileMapper">
<insert id="insertFile" parameterType="com.example.post.model.FileInfo">
INSERT INTO FILEINFO1 (postId, originalFilename, storedFilename, filePath, fileExt)
VALUES (#{postId}, #{originalFilename}, #{storedFilename}, #{filePath}, #{fileExt})
</insert>
<select id="selectFileByPostId" parameterType="int" resultType="com.example.post.model.FileInfo">
SELECT * FROM FILEINFO1 WHERE POSTID = #{num}
</select>
<select id="selectFile" parameterType="int" resultType="com.example.post.model.FileInfo">
SELECT * FROM FILEINFO1 WHERE FILEID = #{num}
</select>
<delete id="deleteFile" parameterType="int">
DELETE FROM FILEINFO1 WHERE fileId = #{fileId}
</delete>
</mapper>
파일 관련 CRUD 작업
실제 jsp 에도 수정을 통해 파일 업로드/다운로드 로직을 추가해준다.
<c:when test="${status == 1}">
<form id="postForm" action="${pageContext.request.contextPath}/Modify?num=${post.num}" method="post" accept-charset="utf-8" enctype="multipart/form-data">
<label for="title">글번호:</label>
<input type="text" id="num" name="num" value="${post.num}" readonly><br><br>
<label for="title">제목:</label>
<input type="text" id="title" name="title" value="${post.title}"><br><br>
<label for="author">작성자:</label>
<input type="text" id="author" name="author" value="${post.author}"><br><br>
<label for="file">파일첨부:</label>
<input type="file" id="file" name="file"><br><br>
<label for="contents">내용:</label><br>
<textarea id="contents" name="contents">${post.contents}</textarea><br><br>
<input type="submit" value="수정">
</form>
...중략...
<script>
document.getElementById('postForm').addEventListener('submit', function(event) {
const fileInput = document.getElementById("file");
const maxSize = 10 * 1024 * 1024;
if (fileInput.files.length > 0){
if(fileInput.files[0].size > maxSize){
alert("10MB 이하만 업로드 가능합니다.")
event.preventDefault();
}
}
});
</script>
Write.jsp, post.jsp 둘다 파일 업로드 내용을 추가시켜준다.
10MB 이상은 업로드가 불가하기에 eventListener 등록을 통해 미리 제출을 방지한다.