상세 컨텐츠

본문 제목

외부파일 드래그 앤 드롭2(File Drag and Drop)

자바스크립트

by e7e 2023. 12. 19. 20:37

본문

외부파일 드래그 앤 드롭1(File Drag and Drop)

글에서  사용자 편의를 위한 확장한  드래그 앤 드롭 파일업로드 화면구현에 대해

필요한 만큼은 알아 보았당. 

 

이제 기능을 좀더 확장해 보장.

조금이라도 포인트에 더 집중하기 위해서, 사용자는 이미지 파일만을 멀티로 선택해서

마우스로 끌어 올 수 있고, 우린 그것을 미리보기 해주고, 체크박스를 통해 

전송여부를 선택할 수 있다는 아주 제한적인 스토리로 구현해 보장!

 

제공기능을 피부로 느껴보기 위해 일단 전체 소스를 아래에 붙이니,

복사/붙여넣기를 통해 아무 웹서버 위에서 실행시켜 먼저 동작을 확인하장!

 

fullSample.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>E7E Drag Drop</title>
    <style>
        #wrapper {
            width: 350px;
        }

        #preview {
            width: 100%;
            height: 300px;
            border: 2px solid pink;
            background-image: url("https://blog.kakaocdn.net/dn/bvQO1s/btraInYH9xW/oekuwou6IKTnzIHKD40ykK/img.png");
            background-size: 110% 110%;
            background-position: -20px -20px;
            overflow: auto;
        }

        #list {
            width: 100%;
            border: 2px solid black;
            height: 200px;
            overflow: auto;
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>

<body>
    <h1>외부파일 끌다노킹 이해</h1>
    <div id="wrapper">

        <div id="preview" ondragover="f_over()" ondrop="f_drop()">
        </div>
        <div id="list">
            <h3>파일 리스트</h3>
            <hr>
        </div>
        <button onclick="f_send()">떤송</button>
    </div>
    <script>
        // 고정 전역변수(동적생성 아닌 DOM)
        const myPreview = document.querySelector("#preview");
        const myList = document.querySelector("#list");

        // DROP 이벤트 처리
        let attachFileList = [];   // 첨부파일 리스트를 위한 전역변수
        let fileIdIndex = 1;       // 첨부파일(이미지) 아이디 시작넘버

        // 체크박스에 체크된 파일만 전송
        function f_send() {

            //AJAX로 파일전송하기 위해 FormData생성
            let formData = new FormData();

            //선택된 파일만 가져오깅
            let selFiles = f_ckFileList()
            selFiles.forEach(selFile => {
                formData.append("myFiles", selFile);
            })

            // FormData 디버깅시 사용하면 Good!
            console.log("************ 서버에 보내려는 파일 *************");
            for (let [key, value] of formData) {
                console.log(key, value);
            }
            console.log("***********************************************");

            let xhr = new XMLHttpRequest();
            xhr.open("post", "/서버사이드URL", true);
            xhr.onreadystatechange = () => {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    console.log("꼭 체크습관", xhr.responseText);
                }
            }
            // 성공이든 실패든 발생하는 onloadend이벤트를 이용, 404처리
            xhr.onloadend = () => {
                if (xhr.status == "404") {
                    Swal.fire("백엔드 맹글고 하는거 맞아용?");
                }
            }

            xhr.onerror = () => {
                console.log("에러났넹" + xhr.status);
            }

            xhr.send(formData);

        }

        // 체크된 체크박스들의 value를 이용 파일 골라내깅
        function f_ckFileList() {
            //체크된 체크박스들
            let ckCheckboxes = myList.querySelectorAll("input:checked");

            //파일 고르깅
            let selFiles = attachFileList.filter(aFile => {
                for (let i = 0; i < ckCheckboxes.length; i++) {
                    if (aFile.name == ckCheckboxes[i].value) {
                        return true;
                    }
                }
                return false;
            })

            return selFiles;
        }

        //파일 중복 여부 체크 함수, 일단 그냥 파일명으로 비교
        function f_isRepeat(pFile) {
            for (let i = 0; i < attachFileList.length; i++) {
                if (pFile.name == attachFileList[i].name) {
                    return true;  // 중복
                }
            }
            return false;      // 안 중복
        }

        // 파일 1개씩 비동기로 읽어서 처리
        function f_readOneFile(pInx, pFile) {

            // 파일중복여부 체크 
            if (f_isRepeat(pFile)) {
                Swal.fire({
                    title: "미안행~~",
                    text: "너가 이미 선택한 파일이얌!!~~ㅠㅠ",
                    icon: "warning"
                });
                return;  // 그냥 종료
            }
            attachFileList.push(pFile);

            // console.log("파일리스트:", attachFileList);

            // 파일 읽어주는 아저씨 생성, 사람은 바이너리 파일을 못 읽음
            let v_fileReader = new FileReader();
            // 끌어온 파일 읽으라고 지시 
            v_fileReader.readAsDataURL(pFile);
            // 파일 내용을 다 읽었다면, load이벤트 발생(비동기)
            // 읽은 결과는 result속성에 담기게 된당.
            v_fileReader.onload = function () {

                //비동기로 처리됨을 확인할 로그
                //   console.log("체에킁:", pInx, pFile);

                //myPreview.innerHTML = "";  // 이전 내용 클리엉
                let v_img = document.createElement("img");
                v_img.setAttribute("src", v_fileReader.result);
                v_img.id = "fId" + fileIdIndex;
                v_img.style.width = "50%";
                v_img.style.height = "50%";
                //img 태그에 줄 id값 증가
                fileIdIndex++;

                // checkbox와 text를 가진 div 맹글어서 추강!
                let fileDetail = document.createElement("div");
                let chkBox = document.createElement("input");
                chkBox.type = "checkbox";
                chkBox.value = pFile.name;
                chkBox.fileId = v_img.id;
                chkBox.checked = true;     // 디폴트 체크
                chkBox.addEventListener("change", function () {
                    //this가 무얼가리키는지 디버깅, 화살표함수로 바꾸면?
                    //console.log("체킁:", this);
                    let selectImg = document.querySelector("#" + this.fileId);
                    if (this.checked) {
                        selectImg.style.display = "inline-block";
                    } else {
                        selectImg.style.display = "none";
                    }
                })

                let txtBox = document.createElement("input");
                txtBox.type = "text";
                txtBox.readOnly = true;
                txtBox.style.border = "none";
                txtBox.value = pFile.name;

                fileDetail.appendChild(chkBox);
                fileDetail.appendChild(txtBox);
                fileDetail.appendChild(document.createElement("br"));

                myPreview.appendChild(v_img);
                myList.appendChild(fileDetail);
                //스크롤바 아래로 내리깅!
                myPreview.scrollTo(0, myPreview.scrollHeight);
                myList.scrollTo(0, myList.scrollHeight);
            }
        }


        //브라우져가 지원하는 파일 자동으로 여는 거 막기 위함
        function f_over() {
            event.preventDefault();
            event.stopPropagation();
        }

        // Drop 이벤트 기본기능 막고, 원하는 기능 넣기
        function f_drop() {
            event.preventDefault();
            event.stopPropagation();

            // 마우스로 끌어온 파일, 일단 1개만
            let v_files = event.dataTransfer.files;
            for (let i = 0; i < v_files.length; i++) {
                f_readOneFile(i, v_files[i]);
            }
        }

        // Drop 영역외에 파일 끌어다 놓았을 때 브라우져 동작막깅
        window.addEventListener("dragover", function () {
            event.preventDefault();
        });

        window.addEventListener("drop", function () {
            event.preventDefault();
        });

    </script>
</body>

</html>

 

실행 해 보면  바로 금방 그냥 알게된당.

마우스로 잔뜩 끌어다 놓으면 해당 갯수만큼 미리보기와 체크박스가

생기고, 이미 있는 파일을 또 끌어다 놓으면 벌써 있다고 신경질을 부리고, 떤송 버튼을

누르면, 백엔드(서버사이드)가 구현되어 있지 않으므로 백엔드 맹글라고 메세지가 뜬당.

console을 확인해서 떤송 버튼 클릭시, 체크된 파일만이 로그에 찍히는 걸 꼬옥!!

확인해야 한당. 그 파일들만을 전송하려는 게 우리의 목적이었기 때문에

클라이언트 사이드에서는 원하는 목적을 위해 할 일을 다 한 거시당.!

 

이제 코드에서 몇몇 핵심 포인트와 도움이 될 만한 부분을 확인해 보장.

 

 // DROP 이벤트 처리, 전역변수
 let attachFileList = [];   // 첨부파일 리스트를 위한 전역변수
 let fileIdIndex = 1;       // 첨부파일 미리보기(이미지) 아이디 시작넘버

attachFileList는 마우스로 끌고 온 파일들을 담아둘 변수고,

fileIdIndex는 미리보기 img태그에 중복되지 않는 id를 주기위한 전역변수

보이고(display:block), 안보이고(display:none)를 컨트롤하기 위함이당.

 

// Drop 이벤트 기본기능 막고, 원하는 기능 넣기
function f_drop() {
    event.preventDefault();
    event.stopPropagation();

    // 마우스로 끌어온 파일, 일단 1개만
    let v_files = event.dataTransfer.files;
    for (let i = 0; i < v_files.length; i++) {
        f_readOneFile(i, v_files[i]);
    }
}

사용자가 마우스로 파일들을 끌고와서 놓으면, drop이벤트가 발생하고

f_drop함수가 실행된당.  여기서 개별파일들을 f_readOneFile함수에 매개변수로

넘긴당. 이렇게 별도 함수로 맹글어서 넘기는 이유는, 보통 let을 쓰면 문제

없는데, for문에 var를 쓰고, FileReader를 그 안에 쓰면 FileReader가 비동기라서

원치 않는 결과가 발생한당. var를 썼을 때도 문제없도록 별도 function으로 구현

하여 변수 scope(범위)를 확실하게 나누었당.(오켕? 어려울 수 있당!)

 

// 파일 1개씩 비동기로 읽어서 처리
function f_readOneFile(pInx, pFile) {

    // 파일중복여부 체크 
    if (f_isRepeat(pFile)) {
        Swal.fire({
            title: "미안행~~",
            text: "너가 이미 선택한 파일이얌!!~~ㅠㅠ",
            icon: "warning"
        });
        return;  // 그냥 종료
    }
    attachFileList.push(pFile);

    //생략....

    v_fileReader.onload = function () {

        //생략.....
        //img 태그에 줄 id값 증가
        v_img.id = "fId" + fileIdIndex;
        fileIdIndex++;

        // checkbox와 text를 가진 div 맹글어서 추강!
        // 생략....
        chkBox.fileId = v_img.id;
        chkBox.checked = true;     // 디폴트 체크
        chkBox.addEventListener("change", function () {
            //this가 무얼가리키는지 디버깅, 화살표함수로 바꾸면?
            //console.log("체킁:", this);
            let selectImg = document.querySelector("#" + this.fileId);
            if (this.checked) {
                selectImg.style.display = "inline-block";
            } else {
                selectImg.style.display = "none";
            }
        })

        //생략 .....
        myPreview.scrollTo(0, myPreview.scrollHeight);
        myList.scrollTo(0, myList.scrollHeight);
    }
}

 f_readOneFile 함수 안에서의 내용은 아래와 같당. (차분히 코드에서 해당부분을 확인!)

1. f_isRepeat함수에 파일을 매개변수로 넘겨 중복체크(f_isRepeat함수 구현도 꼭 체킁!)

2. 매개변수로 넘긴 file을 attachFileList에 담는당.

3. 미리보기 img 태그에 고유 id 부여

4. 체크박스에 img태그에 부여한 id값을 fileId속성으로 추가

5. 체크박스 change이벤트에서 체크해제시 img 안 보이겡

                                                  체크시 보이겡

6. img/checkbox/text 추가될 때  스크롤 아래로 내리깅

 

// 체크된 체크박스들의 value를 이용 파일 골라내깅
function f_ckFileList() {
    //체크된 체크박스들
    let ckCheckboxes = myList.querySelectorAll("input:checked");

    //파일 고르깅
    let selFiles = attachFileList.filter(aFile => {
        for (let i = 0; i < ckCheckboxes.length; i++) {
            if (aFile.name == ckCheckboxes[i].value) {
                return true;
            }
        }
        return false;
    })

    return selFiles;
}

f_ckFileList 함수는 체크박스가 체크된 파일들만을 attachFileList에서

뽑아내, 별도 변수 selFiles 담아서 리턴해 주는 함수당.(보면 안당!~~)

 

// 체크박스에 체크된 파일만 전송
function f_send() {

    //AJAX로 파일전송하기 위해 FormData생성
    let formData = new FormData();

    //선택된 파일만 가져오깅
    let selFiles = f_ckFileList()
    selFiles.forEach(selFile => {
        formData.append("myFiles", selFile);
    })

    // FormData 디버깅시 사용하면 Good!
    console.log("************ 서버에 보내려는 파일 *************");
    for (let [key, value] of formData) {
        console.log(key, value);
    }
    console.log("***********************************************");

    let xhr = new XMLHttpRequest();
    xhr.open("post", "/서버사이드URL", true);
    xhr.onreadystatechange = () => {
        if (xhr.readyState == 4 && xhr.status == 200) {
            console.log("꼭 체크습관", xhr.responseText);
        }
    }
    // 성공이든 실패든 발생하는 onloadend이벤트를 이용, 404처리
    xhr.onloadend = () => {
        if (xhr.status == "404") {
            Swal.fire("백엔드 맹글고 하는거 맞아용?");
        }
    }

    xhr.onerror = () => {
        console.log("에러났넹" + xhr.status);
    }

    xhr.send(formData);

}

떤송 버튼 눌렀을 때 실행되는 함수가 f_send() 인데,

f_ckFileList()함수가 돌려준 파일들을  FormData에 담아서

ajax로 서버에 전송하게 된당.(당신은 ajax다린??)

백엔드는 본인이 직접 구현해 보는 거스로 하장!

console 탭을 보면, 체크박스로 선택된 파일들만 출력되는 걸 확인가능하당.

 

한가지 일부러 넣은 부분이 있는데, ajax에서 404에러 메세지를 볼 수있게

onloadend 이벤트(성공/실패와 상관없이 발생)를 활용한 부분을 눈여겨 보장.

(자주 쓰이는 부분은 아니지만, 활용해 보면 재밌을 거시당!)

 

이해가 되었다면, 한번 더 보장...  사실.... 

소스 흐름이 조금만 길어지면, 개인별 취향이 나타나고,

본능적으로 고치고 싶은 부분이  많이 누네 띠게 될 것이다.

고쳐라. 아무도 말리지 않는당.

특히 이벤트 중심 프로그램에선 모아서 일괄처리할지

특정 이벤트마다 필요한 부분을 부분 부분 처리할지 , 고민하고 코딩하고

테스트해보면서 장단점을 몸으로 느끼는 것이 중요하다.

위 소스는 현재 내 감정의 상태를 나타낸당.  정갈하지 못하당(느꼈는강?~)!!

 

화면에서 드래그 앤 드롭을 구현 후 파일 + 알파의 정보를 서버로 보낼 때

개인적인 추천은 AJAX인데,  form태그 전송을 원하는 사람들이 있당.

form태그 전송은 AJAX만큼 flexible하진 못하지만, 

현명한 선택을 위해 요 다음 글에서 한번 해 보도록 하장!

 

당신은 지금 과연 현명한 선택을 하였는가?

 

https://www.youtube.com/watch?v=ylVcGR-T1vU

 

관련글 더보기