[Spring] 게시판 만들기 8 – 파일 업로드/다운로드


게시판 기능에 파일업로드도 추가해보도록 한다.

@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" ?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<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 등록을 통해 미리 제출을 방지한다.

Leave a Comment