자바 스프링 부트 12일차 b01Upload /첨부파일 비동기 업로드 구현 및 파일 정보를 포함한 게시글 등록 처리

2026. 4. 22. 20:05대우개발원 수업 내용/spring boot, framework

반응형

첨부파일 비동기 업로드 구현 및 파일 정보를 포함한 게시글 등록 처리

1. 부트스트랩 기반의 파일 업로드 UI 및 모달 구성 (register.html)

파일 추가 인터페이스: 사용자가 직관적으로 인지할 수 있도록 'ADD Files' 버튼을 배치하고,

클릭 시 파일 선택이 가능한 부트스트랩 모달창이 나타나도록 설계함

미리보기 영역(uploadResult): 업로드가 완료된 파일의 섬네일과 파일명을

카드(Card) 형태로 동적으로 출력하여 사용자가 업로드 상태를 즉시 확인할 수 있게 함

파일 선택 제한: 모달 내의 <input type="file">에 multiple 속성을 부여하여 한 번에 여러 개의 이미지를 선택하고 업로드할 수 있는 환경을 제공함

 

2. Axios를 활용한 비동기 파일 업로드 및 삭제 로직 (upload.js)

uploadToServer: FormData 객체에 담긴 파일 데이터를 multipart/form-data 형식으로

서버의 /upload 엔드포인트에 비동기 전송하고 결과를 반환받음

removeFileToServer: 섬네일의 삭제(X) 버튼 클릭 시, 해당 파일의 UUID와 이름을

파라미터로 전달하여 서버 저장소에서 실제 파일을 즉시 제거함

사용자 경험(UX) 개선: 페이지 새로고침 없이 파일의 추가와 삭제가 이루어지도록 구현하여 빠르고 부드러운 인터랙션을 제공함

 

3. 업로드 결과의 동적 렌더링 및 미리보기 구현 (register.html)

showUpFiles 함수: 서버로부터 전달받은 link(섬네일 경로)와 uuid, fileName 데이터를 활용하여

HTML 문자열을 생성하고 화면에 카드를 동적으로 추가함

데이터 보관(data-src): 나중에 게시글 저장 시 서버로 전송할 파일 식별자(UUID_파일명)를

<img> 태그의 속성에 미리 저장하여 데이터 추출이 용이하게 함

DOM 제어: closest(".card")를 사용하여 특정 삭제 버튼이 속한 카드 요소만 정확히 찾아내어

화면에서 제거하는 효율적인 DOM 관리 로직을 적용함

 

4. 폼 제출 시 첨부파일 데이터의 히든 필드 전환 처리 (register.html)

이벤트 제어: submitBtn 클릭 시 기본 제출 동작을 중단(preventDefault)하고,

현재 화면에 떠 있는 섬네일 리스트에서 파일 정보들을 먼저 수집함

input type='hidden': 수집된 파일 식별자들을 fileNames라는 이름의 숨겨진 입력 요소로 생성하여 폼(form) 내부에 동적으로 삽입

데이터 무결성: 단순한 텍스트 정보(제목, 내용)와 비동기로 미리 업로드된 파일의 경로 정보를 하나로 묶어 최종적으로 서버의 BoardDTO로 전송함

 

5. 데이터베이스 저장 및 최종 등록 결과 검증

멀티 테이블 저장: 서버 계층에서 전달받은 DTO를 엔티티로 변환하여 board 테이블과 board_image 테이블에 연관 관계를 유지하며 데이터를 동시 저장함

등록 완료 알림: 저장이 성공하면 리스트 페이지로 리다이렉트되며, 생성된 게시글 번호를 모달창으로 띄워 사용자에게 작업 성공을 명확히 고지함

최종 확인: DB 조회를 통해 게시글 테이블의 텍스트 데이터와 이미지 테이블의 파일 정보가 외래키(bno)를 기반으로 정확히 일치함을 확인함


[요약]

첨부파일 비동기 업로드 구현 및 파일 정보를 포함한 게시글 등록 처리

1. 부트스트랩 기반의 파일 업로드 UI 및 모달 구성 (register.html)

2. Axios를 활용한 비동기 파일 업로드 및 삭제 로직 (upload.js)

3. 업로드 결과의 동적 렌더링 및 미리보기 구현 (register.html)

4. 폼 제출 시 첨부파일 데이터의 히든 필드 전환 처리 (register.html)

5. 데이터베이스 저장 및 최종 등록 결과 검증

 

CRUD 등록화면 부터 확인

register.html

부트스트랩(Bootstrap) 클래스를 활용하여 사용자가 게시물에 이미지를 추가할 수 있도록 설계된 파일 업로드 버튼 영역의 UI 구조

더보기
<div class="input-group mb-3">
    <span class="input-group-text">Images</span>
    <div class="float-end uploadHidden">
        <button type="button" class="btn btn-primary uploadFileBtn">ADD Files</button>
    </div>
</div>

 


upload.js 코드 추가

Axios 라이브러리를 활용하여 브라우저에서 서버로 파일을 업로드하거나 저장된 파일을 삭제하는 비동기 통신 로직

더보기
// 서버에 파일을 업로드 하는 비동기 함수
async function uploadToServer(fromObj) {
    console.log("upload To Server...."); // 업로드 시작 로그 출력
    console.log(fromObj); // 업로드할 폼 데이터 출력
    // Axios를 이용하여 서버에 파일 업로드 요청
    const response = await axios({
        method: 'POST', // POST 요청
        url: '/upload', // 업로드 API 엔드포인트
        data: fromObj, //업로드할 데이터(FormData 객체)
        headers: {
            'Content-Type': 'multipart/form-data', // 파일 업로드를 위한 헤더 설정
        },
    });
    return response.data; //서버 응답 데이터 반환
}
async function removeFileToServer(uuid, fileName) {
    //Axios를 이용하여 파일 삭제 요청 (DELETE 메서드 사용)
    const response = await axios.delete(`/remove/${uuid}_${fileName}`);
    return response.data; // 서버 응답 데이터 반환
}

첨부파일 추가를 위한 모달창에 대한 코드, 첨부파일 섬네일을 보여줄 부분까지 추가

register.html 코드에 추가

 

더보기
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/basic.html}">
<head>
    <title>Board Register</title>
</head>
<div layout:fragment="content">
    <div class="row mt-3">
        <div class="col">
            <div class="card">
                <div class="card-header">
                    Board Register
                </div>
                <div class="card-body">
                    <form action="/board/register" method="post">
                        <div class="input-group mb-3">
                            <span class="input-group-text">Title</span>
                            <input type="text" name="title" class="form-control" placeholder="Title">
                        </div>
                        <div class="input-group mb-3">
                            <span class="input-group-text">Content</span>
                            <textarea class="form-control col-sm-5" rows="5" name="content"></textarea>
                        </div>
                        <div class="input-group mb-3">
                            <span class="input-group-text">Writer</span>
                            <input type="text" name="writer" class="form-control" placeholder="Writer">
                        </div>
                        <div class="input-group mb-3">
                            <span class="input-group-text">Images</span>
                            <div class="float-end uploadHidden">
                                <button type="button" class="btn btn-primary uploadFileBtn">ADD Files</button>
                            </div>
                        </div>

                        <div class="my-4">
                            <div class="float-end">
                                <button type="submit" class="btn btn-primary">Submit</button>
                                <button type="reset" class="btn btn-secondary">Reset</button>
                            </div>
                        </div>
                    </form>
                </div><!--end card body-->
            </div><!--end card-->
        </div><!-- end col-->
    </div><!-- end row-->
    <!—- 첨부파일 섬네일을 보여줄 부분 -->
    <div class="row mt-3">
        <div class="col ">
            <div class="container-fluid d-flex uploadResult" style="flex-wrap: wrap;">
            </div>
        </div>
    </div>

    <!—- 첨부파일 추가를 위한 모달창 -->
    <div class="modal uploadModal" tabindex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Upload File</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-
                            label="Close"></button>
                </div>
                <div class="modal-body">
                    <div class="input-group mb-3">
                        <input type="file" name="files" class="form-control" multiple >
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-primary uploadBtn">Upload</button>
                    <button type="button" class="btn btn-outline-dark closeUploadBtn"
                    >Close</button>
                </div>
            </div>
        </div>
    </div><!-- register modal -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="/js/upload.js"></script>
</div>



<script layout:fragment="script" th:inline="javascript">
    const errors = [[${errors}]]
    console.log(errors)
    let errorMsg = ''
    if(errors){
        for (let i = 0; i < errors.length; i++) {
            errorMsg += `${errors[i].field}은(는) ${errors[i].code} \n`
        }
        alert(errorMsg)
    }

    //업로드 모달
    const uploadModal = new bootstrap.Modal(document.querySelector(".uploadModal"))
    document.querySelector(".uploadFileBtn").addEventListener("click", function(e){
        e.stopPropagation()
        e.preventDefault()
        uploadModal.show()
    }, false)

</script>

실행하면

업로드 버튼이 생기고 모달창이 생김

 


register.html

브라우저에서 사용자가 선택한 파일을 서버로 전송하고,

성공 시 화면에 미리보기 카드를 동적으로 생성하거나 삭제하는 클라이언트 측 인터랙션 로직

더보기
  // 업로드 버튼 클릭 시 이벤트 처리
    document.querySelector(".uploadBtn").addEventListener("click", function(e) {
        // FormData 객체 생성 (파일 업로드를 위한 객체)
        const formObj = new FormData();
        // 파일 입력 요소 가져오기
        const fileInput = document.querySelector("input[name='files']");
        console.log(fileInput.files); // 선택된 파일 목록 출력
        // 파일 리스트 가져오기
        const files = fileInput.files;
        // 선택된 파일들을 FormData에 추가
        for (let i = 0; i < files.length; i++) {
            formObj.append("files", files[i]);
        }
        // 서버에 파일 업로드 요청
        uploadToServer(formObj).then(result => {
            console.log(result); // 업로드 결과 출력
            // 업로드된 파일 정보를 화면에 표시
            // for (const uploadResult of result) {
            //   showUploadFile(uploadResult);
            // }
            // 업로드 모달 닫기
            uploadModal.hide();
        }).catch(e => {
            // 업로드 실패 시 모달 닫기
            uploadModal.hide();
        });
    }, false);
    // 업로드된 파일을 표시할 요소 가져오기
    const uploadResult = document.querySelector(".uploadResult");

    // 업로드 된 파일 정보를 화면에 표시하는 함수
    function showUpFiles({ uuid, fileName, link }) {
        // 업로드된 파일을 표시할 HTML 문자열 생성
        const str = `<div class="card col-4">
    <div class="card-header d-flex justify-content-center">
        ${fileName} <button class="btn-sm btn-danger" onclick="javascript:removeFile('${uuid}', '${fileName}', this)">X</button>
        </div>
    <div class="card-body">
        <img src="/view/${link}" data-src="${uuid}_${fileName}">
        <!-- 업로드 된 이미지 표시 -->
        </div>
</div><!-- card -->`;

        // 업로드 결과 영역에 추가하여 화면에 표시
        uploadResult.innerHTML += str;
    }
    // 파일 삭제 처리 함수
    function removeFile(uuid, fileName, obj) {
        console.log(uuid);
        console.log(fileName);
        // 삭제 버튼이 속한 카드 요소 찾기. 현재 요소부터 위로 올라가면서 .card 클래스를 가진 부모 요소를 찾음
        const targetDiv = obj.closest(".card") //obj : 이벤트가 발생한 요소 ("X" 버튼)
        // 서버에 파일 삭제 요청 (axios 사용)
        removeFileToServer(uuid, fileName).then(data => {
            // 삭제 성공 시 화면에서 해당 카드 제거
            targetDiv.remove()
        });
    } 

실행하면

파일 업로드 해서 로그가 나오고 섬네일도 뜬다.

파일 삭제를 했을때도 로그와 함께 파일이 정상적으로 삭제된다.


register.html

 submiyBtn

추가

그리고 아래에 코드 추가

더보기
// 제출 버튼 클릭 시 이벤트 처리
document.querySelector(".submitBtn").addEventListener("click", function(e){
    // 기본 동작을 막고, 이벤트 전파를 중지
    e.preventDefault();
    e.stopPropagation()
    // 숨겨진 업로드 파일 입력 요소 가져오기
    const target = document.querySelector(".uploadHidden");
    // 업로드 된 이미지 파일들 가져오기 (업로드 겨로가 영역에서 <img>요소들)
    const uploadFiles = uploadResult.querySelectorAll("img");
    // 숨겨진 입력 요소들을 담을 문자열 초기화
    let str = '';
    // 업로드된 각 파일에 대해 숨겨진 입력 요소 생성
    for (let i = 0; i < uploadFiles.length; i++) {
        const uploadFile = uploadFiles[i];
        const imgLink = uploadFile.getAttribute("data-src"); // 이미지의 data-src 속성 값 가져오기
    }
    // 생성된 숨겨진 입력 요소 문자열에 추가
    target.innerHTML = str;
    // 폼 제출
document.querySelector("form").submit();
}, false);

실행하면

게시글이 등록이 되고 등록이 되었다는 모달 창이 뜨고 게시물이 list페이지에 추가된다

중간에

더보기
// 생성된 숨겨진 입력 요소 문자열에 추가
str += `<input type='hidden' name='fileNames' value="${imgLink}">`;

를 추가하면

이제 

등록했을때 사진과 글 모두 테이블에 저장되는것을 확인