본문 바로가기
Spring Boot

스프링 부트(Spring Boot) 게시판 - 첨부파일 추가/수정/삭제 및 기존 파일 유지하기 (다중 파일 업로드 & 다운로드 구현하기 2/3) [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

by 도뎡 2023. 6. 3.
반응형
  • 본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.
  • DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기)
  • 화면 처리는 HTML5 기반의 자바 템플릿 엔진인 타임리프(Thymeleaf)를 사용합니다.

이전 글에서는 첨부파일 관리용 테이블(tb_file)을 생성하고, 애플리케이션 전역에서 사용할 수 있는 파일 업로드용 유틸 클래스를 구현해서 첨부파일을 다중으로 업로드하는 방법을 알아보았습니다.

이번에는 게시글에 등록된 첨부파일을 상세 페이지에 출력해 주는 기능을 우선적으로 구현하고, 기존에 첨부파일이 업로드된 게시글을 수정할 때 첨부파일을 추가/변경/삭제하는 방법과, 파일에 변화가 없을 때 기존 첨부파일을 유지하는 방법을 알아보도록 하겠습니다.

 

1. 파일 응답(Response) 클래스 생성하기

가장 먼저, DB에 저장된 파일 정보를 조회하는 용도의 응답 클래스입니다. 여기서 말씀드리는 파일 정보는 실제로 디스크에 업로드된 후 파일 테이블(tb_file)에 저장되는 정보를 의미합니다.

package com.study.domain.file;

import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class FileResponse {

    private Long id;                      // 파일 번호 (PK)
    private Long postId;                  // 게시글 번호 (FK)
    private String originalName;          // 원본 파일명
    private String saveName;              // 저장 파일명
    private long size;                    // 파일 크기
    private Boolean deleteYn;             // 삭제 여부
    private LocalDateTime createdDate;    // 생성일시
    private LocalDateTime deletedDate;    // 삭제일시

}

 

2. FileMapper 인터페이스 - 메서드 추가하기

FileMapper 인터페이스에 아래 세 개의 메서드를 추가해 주세요.

    /**
     * 파일 리스트 조회
     * @param postId - 게시글 번호 (FK)
     * @return 파일 리스트
     */
    List<FileResponse> findAllByPostId(Long postId);

    /**
     * 파일 리스트 조회
     * @param ids - PK 리스트
     * @return 파일 리스트
     */
    List<FileResponse> findAllByIds(List<Long> ids);

    /**
     * 파일 삭제
     * @param ids - PK 리스트
     */
    void deleteAllByIds(List<Long> ids);
메서드 설명
findAllByPostId( ) 게시글 번호(postId)를 기준으로 게시글에 등록된 모든 첨부파일을 조회합니다.
findAllByIds( ) 리스트 타입의 파일 번호(ids)를 기준으로 여러 개의 첨부파일을 조회합니다. 이 메서드는 물리적 파일의 삭제 처리에 사용되며, 용도는 뒤에서 설명드리도록 하겠습니다.
deleteAllByIds( ) 리스트 타입의 파일 번호(ids)를 기준으로 DB에서 첨부파일을 삭제 처리합니다.

 

3. FileMapper XML - SQL 쿼리 작성하기

FileMapper.xml에 FileMapper 인터페이스에 추가한 메서드들과 연결할 SQL 쿼리를 작성해 주세요.

    <!-- 파일 리스트 조회 -->
    <select id="findAllByPostId" parameterType="long" resultType="com.study.domain.file.FileResponse">
        SELECT
            <include refid="fileColumns" />
        FROM
            tb_file
        WHERE
            delete_yn = 0
            AND post_id = #{value}
        ORDER BY
            id
    </select>


    <!-- 파일 리스트 조회 -->
    <select id="findAllByIds" parameterType="list" resultType="com.study.domain.file.FileResponse">
        SELECT
            <include refid="fileColumns" />
        FROM
            tb_file
        WHERE
            delete_yn = 0
            AND id IN
            <foreach item="id" collection="list" open="(" separator="," close=")">
            #{id}
            </foreach>
        ORDER BY
            id
    </select>


    <!-- 파일 삭제 -->
    <delete id="deleteAllByIds" parameterType="list">
        UPDATE tb_file
        SET
              delete_yn = 1
            , deleted_date = NOW()
        WHERE
            id IN
            <foreach item="id" collection="list" open="(" separator="," close=")">
            #{id}
            </foreach>
    </delete>
SQL ID 설명
findAllByPostId 게시글에 업로드된 모든 첨부파일을 조회합니다. 게시글과 댓글은 리스트 조회 쿼리의 정렬(ORDER BY) 기준을 id 내림차순(DESC)으로 했지만, 첨부파일은 업로드한 순서가 유지되어야 하기 때문에 id 오름차순(ASC)으로 정렬합니다.
findAllByIds MyBatis의 foreach를 이용해서 ids에 해당되는 모든 첨부파일을 조회합니다. 앞에서 말씀드렸듯이 용도는 뒤에서 설명드리겠습니다.
deleteAllByIds MyBatis의 foreach를 이용해서 ids에 해당되는 모든 첨부파일을 삭제 처리합니다.

 

4. FileService 클래스 - 첨부파일 조회 메서드 추가하기

SQL 쿼리까지 작성하셨다면 다음은 서비스입니다. 특별한 비즈니스 로직 없이 Mapper의 메서드를 리턴 또는 실행하는 게 전부입니다.

findAllFileByIds( )와 deleteAllFileByIds( )는 ids가 비어있지 않은 경우에만 쿼리를 실행하며, findAllByIds( )는 ids가 비어있는 경우, 사이즈가 0인 비어있는 리스트를 리턴합니다.

    /**
     * 파일 리스트 조회
     * @param postId - 게시글 번호 (FK)
     * @return 파일 리스트
     */
    public List<FileResponse> findAllFileByPostId(final Long postId) {
        return fileMapper.findAllByPostId(postId);
    }

    /**
     * 파일 리스트 조회
     * @param ids - PK 리스트
     * @return 파일 리스트
     */
    public List<FileResponse> findAllFileByIds(final List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return fileMapper.findAllByIds(ids);
    }

    /**
     * 파일 삭제 (from Database)
     * @param ids - PK 리스트
     */
    @Transactional
    public void deleteAllFileByIds(final List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return;
        }
        fileMapper.deleteAllByIds(ids);
    }

 

5. 파일 컨트롤러(Controller) 클래스 생성하기

댓글과 마찬가지로 파일도 REST API 방식으로 데이터만 주고받습니다. 사실 PostController에 FileService를 DI(의존성 주입) 해서 처리해도 되지만, 도메인 단위로 영역을 확실히 분리하기 위해서 파일용 컨트롤러를 따로 두고자 합니다.

package com.study.domain.file;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class FileApiController {

    private final FileService fileService;

    // 파일 리스트 조회
    @GetMapping("/posts/{postId}/files")
    public List<FileResponse> findAllFileByPostId(@PathVariable final Long postId) {
        return fileService.findAllFileByPostId(postId);
    }

}

 

6. view.html - 게시글 상세 페이지 수정하기

마지막으로 상세 페이지에 첨부파일 영역을 추가하고, FileApiController에 선언한 API 메서드를 호출해 주면 첨부파일 조회 처리는 끝이 납니다.

지금부터 6-N번을 순서대로 진행해 주시면 되는데요. 전체 소스를 참고하고 싶으시다면, 본문 끝에서 Board.zip을 다운로드해 주세요.

 

6-1) 첨부파일 영역(HTML) 추가하기

콘텐츠 영역에서 게시글 내용을 출력하는 tr 태그 밑에 다음의 코드를 추가해 주세요.

    <tr>
        <th scope="row">첨부파일</th>
        <td id="files" colspan="3">
    
        </td>
    </tr>

 

6-2) 첨부파일 조회용 함수 선언 & 호출하기

자바스크립트 영역에 findAllFile( )을 추가하고, 추가한 함수를 onload( )에서 호출하도록 코드를 변경해 주세요.

    window.onload = () => {
        findAllFile();

        findAllComment();
    }


    // 전체 파일 조회
    function findAllFile() {

        // 1. API 호출
        const postId = [[ ${post.id}]];
        const response = getJson(`/posts/${postId}/files`);

        // 2. 로직 종료
        if ( !response.length ) {
            return false;
        }

        // 3. 파일 영역 추가
        let fileHtml = '<div class="file_down"><div class="cont">';
        response.forEach(row => {
            fileHtml += `<a href="javascript:alert('준비 중입니다.');"><span class="icons"><i class="fas fa-folder-open"></i></span>${row.originalName}</a>`;
        })
        fileHtml += '</div></div>';

        // 4. 파일 HTML 렌더링
        document.getElementById('files').innerHTML = fileHtml;
    }

 

아래 이미지는 브라우저 개발자 도구 콘솔에 파일 응답 데이터(response)를 출력해 본 결과입니다. 저는 이전 글에서 32357번 게시글에 총 세 개의 파일을 업로드했고, 게시글에 등록된 모든 첨부파일 정보가 정상적으로 내려오고 있습니다.

FileApiController - findAllFileByPostId( ) 호출 결과

 

fileAllFile( ) 로직 번호 설명
1번 FileApiController의 findAllFileByPostId( )를 호출해서 게시글에 등록된 모든 첨부파일을 조회합니다. 첨부파일이 있는 경우, 변수 response에는 컨트롤러 메서드의 리턴 타입인 List<FileResponse> 타입의 객체 배열이 담깁니다.

API 통신 시에는 항상 응답으로 내려오는 데이터를 개발자 도구 콘솔에 찍어보는 습관을 가지면 개발 속도 향상에 도움이 됩니다.
2번 게시글에 등록된 첨부파일이 없는 경우를 뜻합니다. 이 때 response는 빈 배열(Empty array)이 되는데요. if 문 안의 조건은, '배열의 사이즈가 1보다 작다(response.length < 1)'와 같은 뜻을 가집니다.
3번 fileHtml에 첨부파일 HTML을 추가합니다. 여기서 포인트는 response에 담긴 모든 파일 응답 객체를 순환해서 그리는 a 태그입니다. 우리는 추후에 이 a 태그를 통해 첨부파일을 다운로드 하게 됩니다.
4번 앞에서 추가한 첨부파일 영역에 3번에서 그린 HTML을 렌더링합니다.

 

6-3) 상세 페이지 접속해 보기

여기까지 잘 따라오셨다면, 게시글 상세 페이지에 첨부파일 영역이 추가된 걸 확인하실 수 있습니다. 

상세 페이지 - 첨부파일 업로드 O

 

상세 페이지 - 첨부파일 업로드 X

 

7. write.html - 게시글 등록 페이지 수정하기

6번을 끝으로 상세 페이지에 첨부파일 목록을 출력해 주는 기능은 마무리되었습니다. 지금부터 게시글을 수정하는 시점에 파일을 추가/변경/삭제하는 방법을 설명드리도록 하겠습니다. 여기서부터는 어느 정도의 집중력이 필요하니 잘 따라와 주시기 바랍니다.

 

7-1) 첨부파일 조회용 함수 선언 & 호출하기

게시글을 수정하는 시점에 기존에 등록된 첨부파일이 있다면, view.html과 마찬가지로 이미 등록된 첨부파일을 사용자에게 보여주어야 합니다.

write.html에 findAllFile( )을 추가하고, 추가한 함수를 onload( )에서 호출하도록 자바스크립트 코드를 변경해 주세요.

    window.onload = () => {
        renderPostInfo();

        findAllFile();
    }


    // 전체 파일 조회
    function findAllFile() {

        // 1. 신규 등록/수정 체크
        const post = [[ ${post}]];
        if ( !post ) {
            return false;
        }

        // 2. API 호출
        const response = getJson(`/posts/${post.id}/files`);

        // 3. 로직 종료
        if ( !response.length ) {
            return false;
        }

        // 4. 업로드 영역 추가
        for (let i = 0, len = (response.length - 1); i < len; i++) {
            addFile();
        }

        // 5. 파일 선택 & 삭제 이벤트 재선언 & 파일명 세팅
        const filenameInputs = document.querySelectorAll('.file_list input[type="text"]');
        filenameInputs.forEach((input, i) => {
            const fileInput = input.nextElementSibling.firstElementChild;
            const fileRemoveBtn = input.parentElement.nextElementSibling;
            fileInput.setAttribute('onchange', `selectFile(this, ${response[i].id})`);
            fileRemoveBtn.setAttribute('onclick', `removeFile(this, ${response[i].id})`);
            input.value = response[i].originalName;
        })
    }

 

findAllFile( )

1번 로직은 게시글의 신규 등록/수정을 체크합니다. 신규 등록인 경우에는 첨부파일을 조회할 필요가 없기 때문에 로직을 종료합니다. 2번, 3번 로직은 view.html에 선언한 findAllFile( )의 1, 2번 로직과 거의 동일합니다.

여기서는 4번 로직부터 설명드릴 건데요. 설명에 앞서, 4번 로직의 for 문 안에서 실행되는 addFile( )의 함수 구조는 아래 코드와 같습니다. 이 함수는 첨부파일 영역에 업로드 HTML을 추가해 주는 기능을 합니다.

    // 파일 추가
    function addFile() {
        const fileDiv = document.createElement('div');
        fileDiv.innerHTML =`
            <div class="file_input">
                <input type="text" readonly />
                <label> 첨부파일
                    <input type="file" name="files" onchange="selectFile(this);" />
                </label>
            </div>
            <button type="button" onclick="removeFile(this);" class="btns del_btn"><span>삭제</span></button>
        `;
        document.querySelector('.file_list').appendChild(fileDiv);
    }

 

여기서 포인트는 for 문의 반복 조건을 (업로드된 첨부파일 개수(response.length) - 1)로 선언했다는 점인데요. write.html의 입력 폼(saveForm)에는 기본적으로 메인 첨부파일 영역이 있기 때문에, 업로드된 첨부파일 개수보다 1개 적게 파일 영역이 추가되어야 합니다.

글쓰기 페이지 - 메인 첨부파일과 addFile( )로 추가한 첨부파일 영역

 

다음은 5번 로직입니다. filenameInputs에는 바로 위 이미지를 기준으로 첨부파일 업로드 버튼 좌측에 있는 text 타입의 input 태그들이 담깁니다.

filenameInputs 구조

 

forEach( )는 filenameInputs의 길이(length)만큼 반복되는데요. 여기서 포인트는 4번 로직이 종료된 시점에 첨부파일 영역의 개수와 response에 담긴 파일 응답 데이터의 개수가 같다는 점입니다. 즉, forEach( )의 'input'과 'response[ i ]'는 1:1로 매칭됩니다.

filenameInputs를 순환하며 실행되는 메인 로직에서 변수 fileInput은 file 타입의 input을 의미하며, 이 엘리먼트는 파일 선택창에서 파일을 업로드하는 시점에 selectFile( )을 실행하고, fileRemoveBtn은 파일 삭제 버튼을 의미하며, 이 엘리먼트는 업로드된 첨부파일을 삭제하는 시점에 removeFile( )을 실행합니다.

기존에는 두 엘리먼트 모두 함수의 인자로 자신을 의미하는 'this'만 전달했으나, 기존에 업로드된 첨부파일의 변경(수정)과 삭제 처리를 위해 setAttribute( )로 함수를 재선언하는 과정에서 파일의 id 값을 함께 전달합니다.

마지막으로 DB에 저장된 원본 파일명(originalName)을 text 타입의 input value로 세팅합니다.

신규 게시글 작성 시 selectFile( ), removeFile( )로 전달하는 인자

 

게시글 수정 시 selectFile( ), removeFile( )로 전달하는 인자

 

7-2) 첨부파일 삭제 처리용 익명 함수 선언하기

'익명 함수'는 자바스크립트에서 전역 변수의 문제점을 해결하는 방법 중 하나입니다. 우리는 기존에 업로드된 첨부파일이 삭제될 때 삭제된 파일의 id(PK)를 담는 용도로 익명 함수를 이용합니다.


김여름 님의 블로그익명 함수, 전역 변수의 문제점, 그리고 전역 변수를 피하는 방법에 대한 내용이 정말 심플하게 정리되어 있으니, 한 번쯤은 읽어보시기를 권장드립니다.


write.html 자바스크립트 영역에 removeFileId를 선언해 주시면 되는데요. 익명 함수는 함수 선언 위치에 영향을 받으니, selectFile( )과 removeFile( ) 함수보다 앞쪽에 선언해 주세요.

    // 파일 삭제 처리용 익명 함수
    const removeFileId = (function() {
        const ids = [];
        return {
            add(id) {
                if (ids.includes(id)) {
                    return false;
                }
                ids.push(id);
            },
            getAll() {
                return ids;
            }
        }
    }());

 

구성 요소 설명
ids 첨부파일이 변경 또는 삭제되었을 때, 삭제 처리할 첨부파일의 id를 저장하는 용도의 배열(Array)입니다.
add( ) ids에 삭제 처리할 첨부파일 id를 추가합니다. ids.includes( )를 이용하면 배열 안에 중복되는 값이 있는지 체크할 수 있으며, 중복되는 값이 없는 경우에만 ids에 id를 추가합니다.
getAll( ) ids에 담긴, 변경 또는 삭제된 모든 파일의 id를 조회합니다.

 

7-3) selectFile( ) 함수 수정하기

이제, 기존에 업로드된 파일이 변경되면 함수의 파라미터로 this와 파일의 id가 함께 넘어오니, 파일 id를 파라미터로 선언한 후 4번 로직을 작성해 주세요.

    // 파일 선택
    function selectFile(element, id) {

        const file = element.files[0];
        const filename = element.closest('.file_input').firstElementChild;

        // 1. 파일 선택 창에서 취소 버튼이 클릭된 경우
        if ( !file ) {
            filename.value = '';
            return false;
        }

        // 2. 파일 크기가 10MB를 초과하는 경우
        const fileSize = Math.floor(file.size / 1024 / 1024);
        if (fileSize > 10) {
            alert('10MB 이하의 파일로 업로드해 주세요.');
            filename.value = '';
            element.value = '';
            return false;
        }

        // 3. 파일명 지정
        filename.value = file.name;

        // 4. 삭제할 파일 id 추가
        if (id) {
            removeFileId.add(id);
        }
    }

 

 

4번 로직

신규 게시글을 등록하는 시점에는 파일 id가 넘어올 일이 절대로 없기 때문에 if 조건으로 id가 넘어왔는지 체크합니다. 기존에 업로드된 파일이 변경되면, 익명 함수 removeFileId의 ids에 삭제 처리할 첨부파일의 id를 추가합니다. 

 

7-4) removeFile( ) 함수 수정하기

7-3과 마찬가지로 기존에 업로드된 파일이 삭제되면 파일의 id가 파라미터로 넘어오니, 파일 id를 파라미터로 추가한 후 1번 로직을 작성해 주세요.

    // 파일 삭제
    function removeFile(element, id) {
        
        // 1. 삭제할 파일 id 추가 
        if (id) {
            removeFileId.add(id);
        }
        
        // 2. 파일 영역 초기화 & 삭제
        const fileAddBtn = element.nextElementSibling;
        if (fileAddBtn) {
            const inputs = element.previousElementSibling.querySelectorAll('input');
            inputs.forEach(input => input.value = '')
            return false;
        }
        element.parentElement.remove();
    }

 

로직 설명

추가된 1번 로직은 익명 함수인 removeFileId의 ids에 삭제할 첨부파일의 id를 추가합니다. 2번 로직은 기존 코드 그대로입니다. 삭제 버튼 우측에 파일 추가 버튼이 있는 경우에는 해당 파일 영역의 모든 input value를 초기화하고, 아닌 경우에는 파일 영역 자체를 DOM에서 제거합니다.

 

8. FileUtils 클래스 - 첨부파일 삭제 메서드 추가하기

FileUtils에 아래 세 개 메서드를 추가해 주세요.

    /**
     * 파일 삭제 (from Disk)
     * @param files - 삭제할 파일 정보 List
     */
    public void deleteFiles(final List<FileResponse> files) {
        if (CollectionUtils.isEmpty(files)) {
            return;
        }
        for (FileResponse file : files) {
            String uploadedDate = file.getCreatedDate().toLocalDate().format(DateTimeFormatter.ofPattern("yyMMdd"));
            deleteFile(uploadedDate, file.getSaveName());
        }
    }

    /**
     * 파일 삭제 (from Disk)
     * @param addPath - 추가 경로
     * @param filename - 파일명
     */
    private void deleteFile(final String addPath, final String filename) {
        String filePath = Paths.get(uploadPath, addPath, filename).toString();
        deleteFile(filePath);
    }

    /**
     * 파일 삭제 (from Disk)
     * @param filePath - 파일 경로
     */
    private void deleteFile(final String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            file.delete();
        }
    }

 

메서드 설명
deleteFiles( ) DB에서 조회한 삭제할 파일 정보를 전달받아 디스크에서 파일을 삭제 처리합니다. 변수 uploadedDate는 DB에 파일 정보가 저장된 시간(created_date)을 '연월일(yyMMdd)' 형식으로 포맷한 값을 의미합니다.

나머지는 아래 deleteFile( )에서 설명드리도록 하겠습니다.
첫 번째 deleteFile( ) 추가 경로와 디스크에 저장된 파일명을 기준으로 파일을 삭제 처리합니다. addpath는 업로드 연월일 폴더를 처리하기 위해 사용되는 파라미터로, deleteFiles( )의 uploadedDate를 의미합니다.

FileUtils 클래스의 uploadFile( )은 파일을 업로드하는 시점에 오늘 날짜를 기준으로 연월일 폴더를 생성하고, 생성된 연월일 폴더에 파일을 Write 하기 때문에 addPath는 필수 파라미터가 됩니다.

filename은 디스크에 저장된 파일명을 의미합니다. 이 또한 파일 테이블의 save_name을 통해 알 수 있습니다.
두 번째 deleteFile( ) 파일의 전체 경로(디렉터리 경로 + 파일명)를 전달받아 파일을 삭제 처리합니다. file.exists( )로 파일이 존재하는지 확인한 후 file.delete( )로 물리적 파일을 디스크에서 완전히 삭제합니다.

 

9. PostRequest 클래스 - 첨부파일 삭제용 파라미터 선언하기

첨부파일의 삭제를 위해서는 write.html에 선언한 removeFileId의 ids가 필요합니다. 게시글 수정 시점에 ids를 파라미터로 수집할 수 있도록 PostRequest에 removeFileIds를 멤버 변수로 추가해 주세요.

private List<Long> removeFileIds = new ArrayList<>(); // 삭제할 첨부파일 id List

 

10. PostController 클래스 - 게시글 수정 메서드 수정하기

PostController의 updatePost( )를 다음과 같이 변경해 주세요.

    // 기존 게시글 수정
    @PostMapping("/post/update.do")
    public String updatePost(final PostRequest params, final SearchDto queryParams, Model model) {

        // 1. 게시글 정보 수정
        postService.updatePost(params);

        // 2. 파일 업로드 (to disk)
        List<FileRequest> uploadFiles = fileUtils.uploadFiles(params.getFiles());

        // 3. 파일 정보 저장 (to database)
        fileService.saveFiles(params.getId(), uploadFiles);

        // 4. 삭제할 파일 정보 조회 (from database)
        List<FileResponse> deleteFiles = fileService.findAllFileByIds(params.getRemoveFileIds());

        // 5. 파일 삭제 (from disk)
        fileUtils.deleteFiles(deleteFiles);

        // 6. 파일 삭제 (from database)
        fileService.deleteAllFileByIds(params.getRemoveFileIds());

        MessageDto message = new MessageDto("게시글 수정이 완료되었습니다.", "/post/list.do", RequestMethod.GET, queryParamsToMap(queryParams));
        return showMessageAndRedirect(message, model);
    }

 

로직 번호 설명
1번 게시글을 UPDATE 합니다.
2번 게시글을 수정하는 시점에 새로 추가된 파일을 디스크에 업로드합니다.
3번 게시글을 수정하는 시점에 새로 추가된 파일 정보를 DB에 저장합니다.
4번 게시글을 수정하는 시점에 삭제된 파일 정보를 DB에서 조회합니다.
5번 게시글을 수정하는 시점에 삭제된 파일을 디스크에서 삭제합니다.
6번 게시글을 수정하는 시점에 삭제된 파일을 DB에서 삭제 처리합니다.

 

11. write.html - 첨부파일 삭제용 파라미터 처리하기

마지막으로, 게시글 수정을 요청했을 때 PostController의 updatePost( )의 파라미터인 PostRequest에서 삭제할 파일 번호(removeFileIds)를 수집할 수 있도록 폼에 파라미터를 추가해 주면 됩니다.

 

11-1) 폼에 hidden 파라미터 선언하기

아래 코드는 write.html에 있는 saveForm의 일부입니다. saveForm에 removeFileIds만 추가해 주세요.

<form id="saveForm" method="post" autocomplete="off" enctype="multipart/form-data">
    <!--/* 게시글 수정인 경우, 서버로 전달할 게시글 번호 (PK) */-->
    <input type="hidden" id="id" name="id" th:if="${post != null}" th:value="${post.id}" />

    <!--/* 서버로 전달할 공지글 여부 */-->
    <input type="hidden" id="noticeYn" name="noticeYn" />
    
    <!--/* 삭제 처리할 파일 번호 */-->
    <input type="hidden" id="removeFileIds" name="removeFileIds" />

 

11-2) savePost( ) 함수 수정하기

아래 코드는 write.html에 있는 savePost( )의 일부입니다. 폼을 서버로 전송하기 전에 11-1에서 추가한 removeFileIds에 삭제할 파일 번호의 값을 세팅하는 끝에서 세 번째 로직을 추가해 주세요. ( // 추가 )

    // 게시글 저장(수정)
    function savePost() {

        ...
        ...
        ...

        document.getElementById('saveBtn').disabled = true;
        form.noticeYn.value = form.isNotice.checked;
        form.removeFileIds.value = removeFileId.getAll().join(); // 추가
        form.action = [[ ${post == null} ]] ? '/post/save.do' : '/post/update.do';
        form.submit();
    }

 

추가된 로직

익명 함수 removeFileId.getAll( )을 이용해서 ids에 담긴 모든 첨부파일 번호를 가져온 후 join( )을 통해 배열에 담긴 모든 값을 콤마(' , ')로 연결합니다.

join( )은 인자로 전달한 값을 기준으로 배열의 모든 값을 문자열로 연결해서 리턴해주는 함수입니다. 인자를 전달하지 않은 경우에는 기본적으로 콤마로 연결됩니다.

컨트롤러로 단순히 String 또는 Long 타입의 데이터를 List 타입의 파라미터로 전송할 땐 '1,2,3,4,5'와 같이 값을 콤마로 연결해 주면 됩니다.

 

12. 게시글 수정해 보기

이제 모든 준비는 끝났으니 첨부파일 추가, 변경(수정), 삭제를 테스트해 볼 차례입니다. 제가 기존에 업로드 한 파일들은 아래와 같습니다.

게시글 수정 페이지 - 기존에 업로드한 첨부파일

 

기존 상태 그대로 저장해 보니 게시글 정보 UPDATE 쿼리만 실행되고, 나머지 로직은 모두 종료됩니다. 추가된 첨부파일도, 삭제된 첨부파일도 없기 때문입니다.

기존에 업로드한 첨부파일 (디스크)

 

다음은 첫 번째 파일을 삭제하고, 두 번째 파일을 졸업증명서로 변경해서 저장해 본 결과입니다.

게시글 수정 페이지 - 1번 삭제, 2번 변경

 

우선 params에서 수집한 파라미터를 설명드리겠습니다. List<MultipartFile> 타입의 files는 file type의 input의 개수만큼 객체를 담게 되는데요. 첫 번째 파일(0)은 삭제, 두 번째 파일(1)은 변경, 세 번째 파일(2)은 유지되어야 합니다.

FileUtils의 uploadFiles( )가 실행될 때 0번과 2번 인덱스에 해당되는 객체들은 multipartFile.isEmpty( )가 true가 되기 때문에 파일을 업로드하지 않고 로직을 종료합니다. 즉, 1번 인덱스에 해당되는 졸업증명서만 업로드됩니다.

removeFileIds는 삭제할 첨부파일 번호입니다. 첫 번째 파일은 삭제했고, 두 번째 파일은 졸업증명서로 변경했기 때문에 id가 1번, 2번인 파일은 삭제되어야 합니다.

PostController - updatePost( )의 params에서 수집한 정보

 

컨트롤러 메서드의 모든 로직이 종료된 후 다시 수정 페이지로 접속해 본 결과입니다.

게시글 수정 페이지 - 1번 삭제, 2번 변경이 적용된 결과

 

디스크에서도 두 PDF 파일 모두 정상적으로 삭제되었습니다.

1번, 2번이 삭제된 기존 디렉터리

 

졸업증명서 또한 오늘 날짜로 생성된 디렉터리에 정상적으로 Write 되었습니다.

졸업증명서가 업로드된 디렉터리

 

마지막으로 Board1.zip, Board2.zip 두 개의 파일을 추가해서 저장해 본 결과입니다.

게시글 수정 페이지 - 두 개 파일 추가

 

상세 페이지에서 확인해 보니 업로드된 파일 모두 정상적으로 조회되고 있습니다.

게시글 상세 페이지 - 두 개 파일 추가

 

디스크에도 정상적으로 업로드되었습니다.

두 개 파일이 추가된 디렉터리

 

마지막으로 파일 테이블을 조회해 본 결과입니다. 데이터 또한 모두 정상적이며, 모든 케이스에서 문제없이 잘 작동하는 듯합니다.

파일 테이블(tb_file) SELECT 결과

 

마치며

여기까지 첨부파일을 추가, 변경, 삭제, 그리고 유지하는 방법을 알아보았습니다. 여러분은 파일을 디스크(디렉터리)에 업로드(또는 삭제)하는 건지, 데이터베이스에 등록(또는 삭제)하는 건지, 처리 대상만 확실히 이해하고 넘어가 주시면 됩니다.

다음 글에서는 게시글에 등록된 첨부파일을 상세 페이지에서 다운로드하는 기능을 구현해 보도록 하겠습니다. 파일 다운로드는 업로드에 비해 수월한 편입니다.

변명이라면 변명이지만, 눈 상태와 잦은 야근으로 너무나도 오랜만에 포스팅을 하게 되었습니다. 기다려 주신 모든 분들께 죄송하다는 말씀을 드리고 싶습니다. 수면 시간을 줄여서라도 2~3일에 한 번은 포스팅해 보도록 노력하겠습니다.

오늘도 방문해 주시고, 기다려 주신 모든 분들께 감사의 말씀을 드립니다. 행복한 한 주 보내세요 :)

Board.zip
0.93MB

반응형

댓글