백엔드/Spring Framework

게시판 구현 / 8-1. '글 등록' 파일 업로드(미리보기, 수정하기, 삭제)

maverick11471 2024. 7. 23. 12:39

프론트 수정

 

글 등록 시 파일첨부를 미리보기 추가, 수정, 삭제하는 방법에 대해 알아보자.


글 등록 시 미리보기 추가

[post.jsp 수정]

 - 1. 선택된 파일을 배열로 변환

 - 2. imageLoader 메소드 호출

 - 3. FileReader를 사용해 Base64로 읽기. 읽은 후 미리보기 이미지와 삭제 버튼을 포함한 div 생성 + 미리보기 영역 추가

    * img, btn, p 를 div 태그에 자식으로 추가

 


 

 - (참고) id = post-form 내용 / id = preview 내용 /   id = uploadFiles 내용

// id = post-form 내용
<form id="post-form" action="/board/post.do" method="post" enctype="multipart/form-data">
    <c:if test="${loginMember.role eq 'ADMIN'}">
        <div class="form-group">
            <label for="type">카테고리</label>
            <select class="form-select" name="type" id="type">
                <option value="free" selected>자유게시판</option>
                <option value="notice">공지사항</option>
            </select>
        </div>
    </c:if>
    <div class="form-group mt-3">
        <label for="title">제목</label>
        <input type="text" class="form-control" id="title" name="title" required>
    </div>
    <div class="form-group mt-3">
        <label for="nickname">작성자</label>
        <input type="text" class="form-control" id="nickname" name="nickname"
               value="${loginMember.nickname}" readonly required>
    </div>
    <div class="form-group mt-3">
        <label for="content">내용</label>
        <textarea class="form-control" id="content" name="content" rows="10" required></textarea>
    </div>
    <div class="form-group mt-3">
        <label for="uploadFiles">파일첨부</label>
         // (참고) id = uploadFiles 내용
        <input class="form-control" type="file" name="uploadFiles" id="uploadFiles" multiple>
         // (참고) id = preview 내용
        <div id="preview" class="mt-3 text-center"
             data-placeholder="파일을 첨부하려면 파일선택 버튼을 누르세요.">
        </div>
    </div>
    <div class="container mt-3 mb-5 w-50 d-flex justify-content-center">
        <button type="submit" class="btn btn-outline-secondary">등록</button>
    </div>
</form>

 

 

 

 - script 동작 구현

<script>
    // 추가된 파일들을 하나씩 담아줄 배열. File 객체가 저장
    // 전역변수로 선언
    const uploadFiles = [];
// DOM 요소가 완전히 로드되기 전에 JavaScript 코드가 실행되면, 
// 해당 요소를 찾지 못하거나 조작할 수 없는 상황이 발생할 수 있습니다.
// 따라서, $(...) 내부에 코드를 작성함으로써, 
// 페이지의 모든 요소가 로드된 후에 안전하게 JavaScript를 실행하도록 보장합니다.
    $(() => {
    // change 이벤트는 <input>, <select>, <textarea>와 같은 폼 요소의 값이 변경되었을 때 발생
        $("#uploadFiles").on("change", (e) => {
            // input에 추가된 파일들 변수로 받기
            const files = e.target.files;

            // 변수로 받아온 파일들 배열로 변환
            // Array.prototype은 모든 배열 객체가 공유하는 메서드를 포함
			// Array 객체의 prototype에 있는 slice 메서드를 참조합니다. 
            // slice 메서드는 배열의 일부분을 새로운 배열로 반환하는 기능을 가지고 있습니다.
            // .call(files):
			// slice 메서드를 files 객체에 호출합니다. 
            // call 메서드는 특정 객체를 this로 설정하여 함수를 호출할 수 있게 해줍니다. 
            // 여기서 files는 일반적으로 FileList 객체로, 이는 HTML 파일 입력 요소에서 
            // 선택된 파일들을 나타냅니다.
			// FileList는 배열과 유사하지만, 실제 배열은 아니기 때문에 Array의 메서드를 
            // 직접 사용할 수 없습니다. slice 메서드를 사용하여 FileList를 배열로 변환합니다.
            const fileArr = Array.prototype.slice.call(files);

            for(file of fileArr) {
                // 미리보기 메소드 호출
                imageLoader(file);
            }
        });

		// dataTransfer를 쓰는 이유: 드로그앤드랍이 편리하기 때문에
	
        // 폼이 서브밋될 때 uploadFiles 배열에 담겨있는 File 객체들을
        // input에 담아서 서브밋
        $("#post-form").on("submit", (e) => {
        // DataTransfer는 HTML5에서 도입된 객체로, 드래그 앤 드롭 API의 일부로 사용됩니다.
		// DataTransfer 객체는 드래그 앤 드롭 작업 중에 데이터(예: 파일)를 전송하는 데 
        // 사용됩니다. 이 객체를 통해 파일 목록을 추가하거나 제거할 수 있습니다.
            let dataTransfer = new DataTransfer();

		// 이 코드는 uploadFiles 배열에 있는 각 파일 객체를 반복하면서 
        // dataTransfer 객체에 추가하는 과정을 수행합니다. 
        // 이를 통해 드래그 앤 드롭 작업에서 파일을 전송할 수 있도록 준비
            for(i in uploadFiles) {
                const file = uploadFiles[i];
                dataTransfer.items.add(file);
            }

            $("#uploadFiles")[0].files = dataTransfer.files;
        });
    });

 

 - 미리보기 처리하는 imageLoader 메소드 구현

    // 미리보기 처리하는 메소드
    // 미리보기될 파일은 서버나 데이터베이스에 저장된 상태가 아니기 때문에
    // 파일 자체를 Base64 인코딩 방식으로 문자열로 변환해서 이미지로 호출해야 된다.
    // 이미지가 들어갈 태그 생성과 파일을 Base64 인코딩
    const imageLoader = (file) => {
        // 추가된 파일 uploadFiles 배열에 담기
        uploadFiles.push(file);

        let reader = new FileReader();

        // reader가 호출되면 실행될 이벤트 등록
        // onload는 FileReader 객체에서 파일 읽기가 완료되었을 때 호출되는 이벤트 핸들러
        reader.onload = (e) => {
            // 이미지를 표출할 img 태그 생성
            let img = document.createElement("img");

            img.classList.add("upload-file");

            // 이미지인지 아닌지 판단
// /(.*?)\./: 이 정규 표현식은 파일 이름의 시작 부분에서 점(.)이 나타나기 전까지의 
// 모든 문자를 비탐욕적으로 매칭합니다. 즉, 파일 이름의 확장자를 찾기 위해 
// 파일 이름의 모든 문자(0개 이상)와 그 뒤에 오는 점(.)을 포함하는 패턴을 찾습니다.            
            if(file.name.toLowerCase().match(/(.*?)\.(jpg|jpeg|png|gif|svg|bmp)$/)) {
// e는 이벤트 객체로, change 이벤트와 같은 사용자 상호작용에 대한 정보를 포함합니다.
// target은 이벤트가 발생한 요소를 참조하고, 
// result는 FileReader 객체가 파일을 읽은 후의 결과를 나타냅니다. 
// 여기서는 파일을 데이터 URL 형식으로 읽은 결과입니다.
// 즉, 사용자가 선택한 파일이 이미지일 경우, FileReader가 해당 파일을 읽어들여 
// 그 결과를 result 속성에 저장합니다.
                img.src = e.target.result;
            } else {
                img.src = "/static/images/defaultFileImg.png";
            }

            // 미리보기 영역에 추가
            // makeDiv 메소드를 호출해서 만들어진 div 자체를 preview 영역에 추가
            // makeDiv 메소드는 아래에 있음
            $("#preview").append(makeDiv(img, file));
        }

        // 파일을 Base64인코딩된 문자열로 로드
        // 이 메소드가 실행되면서 위에서 등록한 onload 이벤트가 함께 동작한다.
        reader.readAsDataURL(file);
    }

 

    * 이미지파일이 DB로 넘어간 것이 아니기 때문에 Base64로 전송해야 한다. 전송 방식은 글자로 변환해 전송하는 방식이다.

 

 - 미리보기 영역에 추가될 div를 생성하는 makeDiv 메소드 구현

    // 미리보기 영역에 추가될 div를 생성하는 메소드
    const makeDiv = (img, file) => {
        let div = document.createElement("div");

        div.classList.add("upload-file-div");

        // 삭제 버튼 추가
        let btn = document.createElement("input");

        btn.classList.add("upload-file-delete-btn");

        btn.setAttribute("type", "button");
        btn.setAttribute("value", "x");
        // 사용자 정의 속성 추가
        // 사용자가 버튼을 클릭할 때 어떤 파일이 삭제될지를 알 수 있도록 
        // 파일 이름을 속성으로 지정하는 것
        btn.setAttribute("deleteFile", file.name);

        // 파일 이름을 표출할 p 태그 생성
        let p = document.createElement("p");

        p.classList.add("upload-file-name");

		// 사용자가 선택한 파일의 이름이 해당 <p> 요소에 텍스트로 나타나게 됩니다.
        p.textContent = file.name;

        // div태그에 img, btn, p 태그 자식으로 추가
        div.appendChild(img);
        div.appendChild(btn);
        div.appendChild(p);

        return div;
    }

 

- x 버튼에 클릭했을 떄 삭제하는 기능 추가

        // x 버튼에 클릭했을 때 삭제하는 기능 추가
        btn.onclick = (e) => {
            // 클릭된 버튼 변수로 받기
            const element = e.target;

			// file.name 얻기
            const deleteFileName = element.getAttribute("deleteFile");

            // 배열에서 파일 삭제
            for(let i = 0; i < uploadFiles.length; i++) {
                if(deleteFileName === uploadFiles[i].name) {
// splice 메서드를 사용하여 uploadFiles 배열에서 인덱스 i에 해당하는 요소를 
// 하나(1) 삭제합니다. 이로 인해 배열에서 해당 파일이 제거됩니다.
                    uploadFiles.splice(i, 1);
                }
            }

            // input에서도 파일 삭제
            // input type="file"은 첨부된 파일들을 fileList 형태로 관리
            // fileList는 File 객체에 바로 담을수 없기 때문에
            // DataTransfer라는 클래스를 사용해서 변환 후에 담아줘야한다.
            let dataTransfer = new DataTransfer();

            for(i in uploadFiles) {
                // uploadFiles 배열에 있는 File 객체를 하나씩 DataTransfer 객체에 담아준다.
                const file = uploadFiles[i];
                dataTransfer.items.add(file);
            }

            // input type="file"에 fileList 형태로 밀어넣기
            $("#uploadFiles")[0].files = dataTransfer.files;

            // 클릭된 btn 태그를 소유하고 있는 부모 div 태그 삭제
            const parentDiv = element.parentNode;
            $(parentDiv).remove();
        }