JAVA/SpringBoot

게시판 프로젝트 - 첨부파일 추가, 수정, 삭제 및 기존 파일 유지

whyHbr 2023. 11. 15. 22:56
728x90
반응형

게시글에 등록된 첨부파일을 상세 페이지에 출력해주는 기능을 구현.

기존에 첨부파일이 업로드된 게시글을 수정할 때 첨부파일을 추가, 변경, 삭제하는 방법과 파일에 변화가 없을 때 기존 첨부파일을 유지하는 방법을 알아볼 것

 

- 파일 응답 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;    // 삭제일시

}


 

 

- 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에서 첨부파일을 삭제 처리한다.

 

- 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>

 

findAllByPostId : 게시글에 업로드된 모든 첨부파일을 조회한다. 게시글과 댓글은 리스트 조회 쿼리의 정렬 (orderby) 기준을 id내림차순으로 했지만 첨부파일은 업로드한 순서가 유지되어야 하기  때문에 id오름차순 으로 정렬

findAllByIds : MyBatis 의 foreach를 이용해 ids에 해당하는 모든 첨부파일을 조회한다. 

deleteAllIds : MyBatis의 foreach를 이용해 ids에 해당하는 모든 첨부파일을 삭제 처리한다.

 

 

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

서비스 단계. 특별한 비즈니스 로직 필요 없이 Mapper의 메서드를 리턴 또는 실행한다.

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

  /**
     * 파일 리스트 조회
     * @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);
    }

    /**
     * 파일 상세정보 조회
     * @param id - PK
     * @return 파일 상세정보
     */
    public FileResponse findFileById(final Long id) {
        return fileMapper.findById(id);
    }



}

 

 

-파일 컨트롤러 클래스 생성

파일도 REST API 방식으로 데이터를 주고 받는다.

package com.study.domain.file;

import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;

@RestController
@RequiredArgsConstructor

public class FileApiController {
    private final FileService fileService;
    private final FileUtils fileUtils;


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

 

 

- 첨부파일 조회용 함수 선언 & 호출

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;
}

 

findAllFile() :

1. API 호출: FileApiController 의 findAllFIleByPostId()를 호출해 게시글에 등록된 모든 첨부파일을 조회한다. 첨부파일이 잇는 경우, 변수 response에는 컨트롤러 메서드의 리턴 타입인 list<FileResponse> 타입의 객체 배열이 담긴다.

2. 로직 종료: 게시글에 등록된 첨부파일이 없는 경우. 이 땐 response는 빈 배열이 되는데 if문 안의 조건은 배열의 사이즈가 1보다 작다( response.length <1) 와 같은 뜻을 가진다 

3. 파일 영역 추가 :fileHtml에 첨부파일 html을 추가한다. 여기서 포인트는 response에 담긴 모든 파일 응답 객체를 순환해서 그리는 a 태그이다. 추후 이 a태그를 통해 첨부파일을 다운로드 한다.

4. 파일 html 렌더링: 앞에서 추가한 첨부파일 영역에 3번에서 그린 html을 렌더링한다.

 

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

게시글을 수정하는 시점에 파일을 추가, 변경, 삭제하는 방법.

 

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

게시글을 수정하는 시점에 기존에 등록된 첨부파일이 있다면 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;
})
}

 

fileAllFile()

 

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

 

- 첨부파일 삭제 처리용 익명 함수 선언

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

write,html자바 스크립트 영역에 removeFileld를 선언해주면 된다. 익명 함수는 함수 선언 위치에 영향을 받으므로 selectFile(), remveFile() 함수보다 앞쪽에 선언한다.

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

ids: 첨부파일이 변경 또는 삭제되었을때 삭제 처리할 첨부파일의 id를 저장하는 용도의 배열이다.

add() : ids에 삭제 처리할 첨부파일 id 를 추가한다. ids.inclueds()를 이용하면 배열 안에 중복되는 값이 있는지 체크할 수 있으며 중복되는 값이 없는 경우에만 ids에 id를 추가한다.

getAll() : ids에 담긴, 변경 또는 삭제된 모든 파일의 id를 조회한다.

 

-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가 넘어왔는지 체크한다. 기존에 업로드된 파일이 변경되면 익명함수 removeFiledId의 ids에 삭제처리할 첨부파일의 id를 추가한다.

 

-removeFile() 함수 수정

기존에 업로드된 파일이 삭제되면 파일의 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에서 제거한다.

 

 

-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();
    }
    }

delefeFiles(): DB에서 조회한 삭제할 파일 정보를 전달받아 디스크에서 파일을 삭제 처리한다. 변수 uploadData는 DB에 파일 정보가 저장된 시간을 연월일 형식으로 포맷한 값을 의미한다.

첫번째 deleteFile() : 추가 경로와 디스크에 저장된 파일명을 기준으로 파일을 삭제처리한다.  addpath는 업로드 연월일 폴더를 처리하기 위해 사용되는 파라미터로deleteFiles()의 uploadedData를 의미한다.

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

filename 은 디스크에 저장된 파일명을 의미한다. 이또한 파일테이블의 save_name을 통해 알 수 있다

두번째 deleteFile() : 파일의 전체 경로를 전달받아 파일을 삭제처리한다. file.exists()로 존재하지 않는지 확인 후 file.delete()로 물리적 파일을 디스크에서 완전히 삭제한다.

 

- Postrequest클래스 , 첨부파일 삭제용 파라미터 선언하기

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

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

 

 

-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 : 게시글을 업데이트

2: 게시글을 수정하는 시점에 새로 추가된 파일을 디스크에 업로드 한다.

3 : 게시글을 수정하는 시점에 새로 추가된 파일 정보를 DB에 저장한다.

4 : 게시글을 수정하는 시점에 삭제된 파일 정보를 DB에서 조회한다

5 : 게시글을 수정하는 시점에 삭제된 파일을 디스크에서 삭제한다.

6: 게시글을 수정하는 시점에 삭제된 파일을 DB 에서 삭제 처리한다.

 

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

게시글 수정을 요청했을 때 postController의 updatePost() 의 파라미터인 postRequest()에서 삭제할 파일 번호(remoevFilelds)를 수집할 수 있도록 폼에 파라미터를 추가한다.

 

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

아래 코드는write,html에 있는 saveForm의 일부이다. saveForm에 removeFilelds만 추가한다

<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" />

.

 

- savePost() 함수 수정

아래 코드는write.html에 있는 savePost()의 일부이다. 폼을 서버로 전송하기 전에 removeFilelds에 삭제할 파일 번호의 값을 세팅하는 로직을 추가한다.

// 게시글 저장(수정)
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();
}

 

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

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

컨트롤러로 단순히 string  또는 long타입의 데이터를 list 타입의 파라미터로 전송할 땐 1,2,3,4 와 같이 콤마로 연결해 주면 된다.

 

 

728x90