728x90

게시글 작성에서 파일 첨부하는 방법을 기록한다. (본 글은 이미지 첨부 방법을 다룸)

 


1. Mysql - file 테이블

CREATE TABLE `tb_file` (
  `id` int NOT NULL AUTO_INCREMENT,
  `board_id` int NOT NULL,
  `original_file_name` varchar(100) NOT NULL,
  `size` int NOT NULL,
  `path` varchar(100) NOT NULL,
  `stored_file_name` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `board_id` (`board_id`),
  CONSTRAINT `tb_file_ibfk_1` FOREIGN KEY (`board_id`) REFERENCES `tb_board` (`id`) ON DELETE CASCADE
)
  • orginal_file_name : 사용자가 저장한 원본 파일이름
  • size : KB단위의 파일의 용량
  • path : 파일이 저장된 위치
  • stred_file_name : 서버에 저장된 uuid로 중복을 제거한 이름

2. FileDTO

@Data
public class FileDTO {
    private Long id;
    private Long boardId;
    private String originalFileName;
    private Long size;
    private String path;
    private String storedFileName;

    public FileDTO(Long boardId, String originalFileName, String storedFileName, Long size, String path) {
        this.boardId = boardId;
        this.originalFileName = originalFileName;
        this.storedFileName = storedFileName;
        this.size = size;
        this.path = path;
    }
}

3. 게시글 작성 Controller

    // 게시글 작성 & 수정
    @PostMapping("/form")
    public String boardSubmit(@Valid Board board, BindingResult bindingResult, Principal principal,
                              @RequestParam(value = "files", required = false) List<MultipartFile> files, Long id) throws IOException, SQLException {
        if (bindingResult.hasErrors() || files.size() > 7) {
            return "board/form";
        }

        String loginUsername = principal.getName();
        Long newBoardId = 0l;

        if (id == null) { // 새 글 작성
            newBoardId = boardService.save(board, loginUsername); // Insert

            // 첨부파일 있을 때
            if(!files.get(0).getOriginalFilename().isEmpty()) {
                for(int i = 0; i < files.size(); i ++) {
                    if(files.get(i).getContentType().contains("image/")) {
                        fileService.saveFile(files.get(i), newBoardId);
                    } else {
                        System.out.println("이미지 타입이 아닙니다");
                    }
                }
            }
        } else { // 기존 글 수정
            boardService.update(board, id); // Update
        }

        return "redirect:/board/list";
    }
  • @Valid : 게시글 작성 중에 제목과 내용의 제약조건(제목 2글자 이상 등)을 지키지 않으면 첫 줄의 if문을 통해 게시글 작성 홈페이지로 다시 돌아간다.
  • !files.get(0).getOriginalFilename().isEmpty() : MultipartFile 타입의 List의 첫 값을 통해 첨부파일의 존재 여부를 확인
  • getContentType을 통해 첨부파일의 형태가 IMAGE인지 확인 후 저장 로직 실행
  • @RequestParam(value = "files", required = false) : 값이 필수가 아닐 때 required = false 작성. (안적어주면 파일 첨부하지 않을 때 오류 Bad request 400 오류 발생)

4. Service

@Service
public class FileService {

    private final FileMapper fileRepository;

    @Autowired
    public FileService(FileMapper fileMapper) {
        this.fileRepository = fileMapper;
    }

    // 게시글 작성 & 수정에서 첨부파일 추가
    public void saveFile(MultipartFile file, Long boardId) throws IOException, SQLException {
        String uuid = UUID.randomUUID().toString(); // 파일명 중복 제거를 위한 uuid
        String originName = file.getOriginalFilename(); // 파일 원본명
        Long fileSize = file.getSize() / 1024; // kb

        String path = "C:/Temp/";
        String newName = "";

        if (originName.lastIndexOf(".") < 0) {
            newName = uuid + originName; // 확장자명이 없을 때
        } else {
            newName = uuid + StringUtils.substring(originName, originName.lastIndexOf(".")); //확장자명 포함
        }

        FileDTO fileDTO = new FileDTO(boardId, originName, newName, fileSize, path + newName);
        int result = fileRepository.insertFile(fileDTO);

        if (result > 0 && !file.getOriginalFilename().isEmpty()) {
            file.transferTo(new File(path + newName));
        }
    }
}
  • UUID를 이용해서 서버에 저장되는 파일명의 중복을 막는다.
  • StringUtils.substring을 이용해 확장자를 포함시켜 저장
  • result값을 받아서 insert가 바르게 진행 됐는지 확인 (바르게 저장 시 1 리턴)
  • MultipartFile.transferTo : if문 조건(result값과 MultipartFile의 null 체크)을 만족하면 서버로 첨부파일(이미지) 저장

5. View

    <main class="flex-shrink-0">
        <div class="container mt-3">
            <h2>게시판</h2>
            <form action="#" th:action="@{/board/form}" th:object="${board}" method="post"
                enctype="multipart/form-data">
                <input type="hidden" th:field="*{id}">
                <input type="hidden" th:field="*{writerId}">
                <div class="mb-3">
                    <label for="title" class="form-label">Title</label>
                    <input type="text" class="form-control"
                        th:classappend="${#fields.hasErrors('title')} ? 'is-invalid'" id="title" th:field="*{title}">
                    <div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" id="validationServer03Feedback"
                        class="invalid-feedback">
                        Title Error
                    </div>
                </div>
                <div class="mb-3">
                    <label for="content" class="form-label">content</label>
                    <textarea class="form-control" id="content" rows="13"
                        th:classappend="${#fields.hasErrors('content')} ? 'is-invalid'"
                        th:field="*{content}"></textarea>
                    <div th:if="${#fields.hasErrors('content')}" th:errors="*{content}" id="validationServer03Feedback"
                        class="invalid-feedback">
                        Content Error
                    </div>
                </div>

                <div id="imageThumbnail">
                </div>

                <!-- button -->
                <div id="uploadForm" th:if="${param.boardId == null}">
                    <div id="uploadElement">
                        <input id="uploadInput" type="file" class="btn btn-outline-primary" name="files" accept="image/*"
                            onchange="setThumbnail(event);" multiple /> <span style="font-size: small;"> * jpeg / png 타입의
                            이미지를
                            7개까지
                            등록해주세요.</span>
                    </div>
                    <a id="reset" class="mt-3 btn btn-danger" onclick="resetImg()">Reset</a>
                </div>
                
                <div class="nav justify-content-end mb-5">
                    <button id="submit" type="submit" class="me-2 btn btn-primary">write</button>
                    <a type="button" class="btn btn-primary" th:href="@{/board/list}">exit</a>
                </div>
            </form>
        </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
        crossorigin="anonymous"></script>
  • th:if를 이용해서 새로운 글 작성 할 때만 이미지 첨부를 가능하게 제약조건 추가 (글 수정으로 글 작성 form으로 진입하는 경우 첨부파일 버튼을 html에서 제거)
  • <form> : enctype 속성을 "multipart/form-data" 로 설정해줘야 파일을 컨트롤러로 전달 가능
  • <div> : id="imageThumbnail"  <-- 여기에 첨부된 파일의 미리보기를 불러올 예정 (썸네일)
  • <input type="file" accept="image/*" multiple/> : accept로 첨부파일 선택 시 모든파일 -> image타입의 파일을 선택하도록 유도. multiple을 사용해야 다중 파일 선택 가능

6. script

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script>
        document.getElementById('submit').addEventListener('click', imageCounting)

        // 첨부 이미지 리셋 버튼
        function resetImg() {
            var fileUpload = document.getElementById('uploadInput')
            var img = document.querySelectorAll('img')
            fileUpload.value = null
            $('#imageThumbnail').children().remove()
        }

        // 이미지 개수 제한
        function imageCounting() {
            var fileUpload = $("input[type='file']")
            if (parseInt(fileUpload.get(0).files.length) > 7) {
                alert("이미지는 최대 7개까지 등록 가능합니다.")
            }
        }

        // 첨부 이미지 미리보기
        function setThumbnail(event) {
            for (var image of event.target.files) {
                var reader = new FileReader()

                reader.onload = function (event) {
                    var img = document.createElement("img")
                    img.className = "img-thumbnail mb-3 me-3"
                    img.name = "thumbnail"
                    img.style = "inline"
                    img.width = 200
                    img.height = 200
                    img.setAttribute("src", event.target.result)
                    document.querySelector("div#imageThumbnail").appendChild(img)
                }
                reader.readAsDataURL(image)
            }
        }
    </script>

 - resetImg (첨부 이미지 리셋, 첨부파일 리셋)

  • fileUpload.value = null 을 통해 지금까지 불러온 첨부파일 값을 null로 설정
  • $('#imageThumbnail').children().remove() : imageThumbnail 아이디의 태그 자식태그를 제거

 - imgCounting (첨부 개수제한)

  • $("input[type='file']") 를 이용해서 타입이 file인 input 태그를 가져와 저장

 - setThumbnail

  • FileReader를 이용해서 이미지 미리보기(썸네일) 정보 셋팅

7. 결과

테스트 전 file테이블 상태

 

 

시연

 

* 왜인지 모르겠으나 녹화할 때 파일 선택창이 안나와서 추가!

시연 후 MySQL 테이블
시연 후 추가된 파일들

8. 게시글 작성, 수정 (일부 코드 수정) : https://black-mint.tistory.com/70

 

[Spring Boot] 게시글 작성, 수정

이전 파일 업로드 글과 연관지어 게시글 작성의 구현을 마무리하고 이를 정리하겠습니다. 파일첨부에 대해서는 언급하지 않겠습니다! 이전 글 : https://black-mint.tistory.com/62?category=982467 [Spring Boot].

black-mint.tistory.com


6번 - setThumbnail 참고 : https://sinna94.tistory.com/entry/JavaScript-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%AF%B8%EB%A6%AC%EB%B3%B4%EA%B8%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0

+ Recent posts