[Spring Boot] 첨부파일 등록 및 삭제 구현

Coco Park·2024년 1월 24일
0

Spring_Boot

목록 보기
13/13

이번에는 게시글 작성 시에 첨부파일을 등록하도록 하는 코드를 구현해볼 것이다.

구현하는데 사용한 방식은 다음과 같다.

1. input 태그로 파일들을 첨부

2. 각 파일들에 대한 랜덤한 UUID를 부여

3. 부여한 파일명을 로컬 드라이브에 저장

4. 랜덤파일명 및 기존파일명을 테이블에 별도로 저장

5. 이후 게시글 상세보기를 할 경우 해당 게시글 번호에 맞는 파일명들을 DB에서 불러와서 호출

View 단의 기존 코드에 다음과 같은 input 태그를 추가해주면 된다.

<div class="form-group">
  	<label for="files">첨부파일</label>
    <input type="file" id="files" name="files" multiple="multiple">
</div>

type을 file로 설정하면 첨부파일을 추가할 수 있도록 하고, multiple 속성을 부여하면 여러 개의 파일을 첨부할 수 있게 된다.

이를 전송할 때 주의해야할 점은 다음과 같다.

1. form 태그에 enctype을 설정해야함.

	<form enctype="multipart/form-data">

2. javascript에서 첨부파일이 있는 경우와 없는 경우를 나누어야 했음. 왜냐하면, 하나의 코드로 합쳐두면 Controller 상에서 multifile이 null인 상태가 되므로 에러가 발생하기 때문이었다.

합쳐져 있는 경우 다음과 같은 에러가 발생했다.

[nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved 
[org.springframework.web.multipart.support.MissingServletRequestPartException: 
Required part 'uploadFiles' is not present.]

이러한 이슈들을 해결한 후의 자바스크립트는 다음과 같다.

// javascipt - PostMapping 게시글 등록에서 파일 등록 //
$("#btn-save").click(function() {
		// PostMapping을 위한 CSRF 설정
        var header = $("meta[name='_csrf_header']").attr("content");
        var token = $("meta[name='_csrf']").attr("content");
		// View 단에서 전송한 파일들
        var inputFiles = $("input[name='files']");
        var files = inputFiles[0].files;
		// 첨부파일 데이터를 포함한 데이터를 보내기 위한 FormData를 선언
        var attached = new FormData();

        for (let i = 0; i < files.length; i++) {
            attached.append("uploadFiles", files[i]);
        }
		// 첨부파일 외의 게시글 제목 및 내용을 전달하기 위한 JSON
        var tempData =
        {
            "title" : $("#title").val(),
            "content" : $("#content").val()
        };
        attached.append("boardDetail", JSON.stringify(tempData));
		// 파일이 있는 경우와 없는 경우로 분기
        if (files.length > 0) {
            $.ajax({
                        type : "post",
                        url : "/board/write",
                        data : attached,
                        beforeSend : function(xhr) {xhr.setRequestHeader(header, token);},
                        processData : false,
                        contentType : false,
                        enctype : "multipart/form-data",
              			// 파일이 있는 경우 enctype을 설정
                        success : function() {
                            console.log("success");
                            window.location.href = "/board";
                        }, fail : function() {
                            console.log("failed");
                        }
            });
        } else {
            $.ajax({
                        type : "post",
                        url : "/board/write2",
                        data : JSON.stringify(tempData),
                        beforeSend : function(xhr) {xhr.setRequestHeader(header, token);},
                        contentType : "application/json; charset=UTF-8",
              			// 파일이 없는 경우 json으로 제목/내용만 전달
                        success : function() {
                            console.log("success");
                            window.location.href = "/board";
                            },
                        fail : function() {
                            console.log("failed");
                        }
            });
        }
    })
// 파일이 있는 경우의 boardWrite //
// 파일이 없는 경우는 이전에 작성한 포스트와 동일함 //
@Transactional
@ResponseBody
@PostMapping(value = "/board/write", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
	// 파일을 전송하기 위한 Parameter를 명시 //
    public String boardWrite(
            HttpServletRequest hsRequest,
            @RequestPart("uploadFiles") MultipartFile[] files,
            // file에 대한 Parameter 설정 - MultipartFile로 데이터를 받음
            @RequestParam("boardDetail") String map) throws ParseException {

        BoardDTO boardDTO = new BoardDTO();
		// ajax에서 String으로 전달된 데이터를 JSON으로 파싱함
        JSONParser jsonParser = new JSONParser();
        JSONObject obj = (JSONObject) jsonParser.parse(map);
        // 전달한 데이터를 바탕으로 DTO 구성
        boardDTO.setTitle((String) obj.get("title"));
        boardDTO.setWriter(SecurityContextHolder.getContext().getAuthentication().getName());
        boardDTO.setContent((String) obj.get("content"));
		// 해당 게시글에 첨부된 파일들에 대한 정보를 저장
        List<AttachedFileDTO> attachedFileDTOList = new ArrayList<>();
		// 각 파일 마다 임의의 파일명을 명시하고, 원래 파일명과 함께 DB에 저장
        // 또한, 임의의 파일명으로 로컬디스크에 저장
        for (MultipartFile mf : files) {
			// 임의의 uuid로 명시
            UUID uuid = UUID.randomUUID();
            String savedFileName = uuid + "_" + mf.getOriginalFilename();
			// 로컬디스크에 저장
            if (mf.getOriginalFilename() != null) {
                File saveFile = new File("D:\\testFolder", savedFileName);
                try {
                    mf.transferTo(saveFile);
                    AttachedFileDTO attachedFileDTO = new AttachedFileDTO();
                    attachedFileDTO.setStoredFileName(savedFileName);
                    attachedFileDTO.setOriginalFileName(mf.getOriginalFilename());
                    attachedFileDTOList.add(attachedFileDTO);
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
            }
        }
		// 게시글을 저장한 후 해당 게시글의 DB 상 ID값을 반환
        // boardRepository.save(boardEntity).getId();
        Long bid = boardService.boardWrite(boardDTO);
		// 반환한 ID와 함께 DB에 저장
        if (attachedFileDTOList.size() > 0) {
            for (AttachedFileDTO attachedFileDTO : attachedFileDTOList) {
                attachedFileDTO.setBid(bid);
                fileService.fileSave(attachedFileDTO);
            }
        }
        return "redirect:/board";
    }

파일을 포함한 게시글 작성

실제 로컬디스크에 저장된 모습

게시글에 파일이 첨부된 경우 클립표시를 해두도록 구현

@GetMapping("/board")
    public String board(Model model) {
        List<BoardDTO> boardDTOList = boardService.boardTotal();
		// 각 게시글의 ID에 대해 파일이 존재하는지 여부를 확인함
        // 이는 파일 정보 테이블에서 ID값으로 검색했을 때 
        // 데이터의 배열이 빈 배열이면 FALSE, 아니라면 TRUE를 반환하도록 해주어 판단
        List<TempBoardDTO> boardmap = new ArrayList<>();
        for (BoardDTO boardDTO : boardDTOList) {
            boardmap.add(new TempBoardDTO(boardDTO, fileService.isFileExists(boardDTO.getBoardId())));
        }
        model.addAttribute("boardList", boardDTOList);
        model.addAttribute("boardMap", boardmap);
        return "board";
    }

TempBoardDTO는 해당 BoardDTO와 첨부파일의 유무를 갖도록 하여 View단에서 참/거짓 유무에 따라 이미지를 출력하도록 구현하기 위해 정의함

{{#isAttached}}
	<img id="attached_{{BoardDTO.boardId}}" src="/images/attached_thumbnail.png" style="width:50px; height:50px;">
{{/isAttached}}

위처럼 첨부파일을 포함하여 작성하였다면, 해당 게시글에 대한 상세정보 조회시 모든 첨부파일의 원본명을 출력하도록 구현하고자 함.

각 게시글의 id에 대해 첨부파일 테이블에서 id를 기준으로 검색한 후 원본 파일 명만 리스트로 받아 이를 View 단으로 보내주면 됨

// 해당 게시물 id에 대한 파일명들을 반환받음 //
List<AttachedFileDTO> attachedFileDTOList = fileService.findAllFiles(bid);
        if (attachedFileDTOList.size() > 0) {
            model.addAttribute("fileExists", true);
            model.addAttribute("files", attachedFileDTOList);
        } else {
            model.addAttribute("fileExists", false);
        }
<h5>=======첨부파일=======</h5>
{{#fileExists}}
    {{#files}}
        <p>첨부파일명 : {{originalFileName}}</p>
    {{/files}}
{{/fileExists}}
{{^fileExists}}
    <p>첨부파일이 존재하지 않습니다.</p>
{{/fileExists}}

첨부파일이 있는 경우 게시글 상세보기

첨부파일이 없는 경우 게시글 상세보기

이처럼 게시글을 작성했다면, 게시글을 삭제하는 경우 파일명에 대한 정보를 DB에서 삭제하는 것 뿐만 아니라 로컬디스크에서도 삭제하게끔 구현할 필요가 있다.

$("#btn-delete").click(function() {
        var writer = $("#writer").val();
        var nowUser = $("#nowId").val();
        var boardId = $("#boardId").val();

        if (writer === nowUser) {
            if (confirm("삭제하시겠습니까?")) {
                $.ajax({
                        type : "get",
                        url : "/board/delete?bid=" + boardId,
                        contentType : "json",
                        success : function () {
                            alert("게시글이 삭제되었습니다.");
                            window.location.href = "/board";
                       }
                });
            }
        } else {
            alert("현재 접속중인 아이디가 작성자와 다릅니다.");
        }
    })

자바스크립트에서는 동일하지만, Controller 상에서 DB와 로컬디스크 상에서의 파일 삭제가 필요하였다.

@ResponseBody
@GetMapping("/board/delete")
    public void boardDetail(@RequestParam(value="bid") Long bid) {
        boardService.boardDelete(bid);
        commentService.commentDeleteByBid(bid);
		// 해당 게시글 id에 대한 첨부파일들을 모두 불러옴
        List<AttachedFileDTO> attachedFileDTOList = fileService.findAllFiles(bid);
        fileService.deleteAllFiles(bid);
		// 불러온 첨부파일에 대해 해당 파일들을 로컬디스크에서도 지워줘야함
        for (AttachedFileDTO attachedFileDTO : attachedFileDTOList) {
            File file = new File("D:/testfolder/" + attachedFileDTO.getStoredFileName());
            if (file.exists()) {
                if (file.delete()) {
                    System.out.println(attachedFileDTO.getOriginalFileName() + " is deleted !!");
                } else {
                    System.out.println(attachedFileDTO.getOriginalFileName() + " is not deleted !!");
                }
            }
        }
    }

View 단에서의 삭제 요청

삭제된 후의 DB

삭제된 후의 로컬디스크

이처럼 input 태그의 file type과 MultipartFile을 통해 게시글에 파일을 첨부할 수 있도록 구현할 수 있었다.

추가로 구현할 사항

파일 첨부에 대해서는 게시글 상세보기에서 첨부파일을 변경하거나 첨부파일이 없는 게시글에 새로 첨부파일을 추가하는 방법에 대해서 구현해보고자 할 것이다.

profile
ヽ(≧□≦)ノ

0개의 댓글