외부파일 드래그 앤 드롭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
Promise(약속) 모르면 바보옹(비동기) (0) | 2024.01.08 |
---|---|
외부파일 드래그 앤 드롭3 (File Drag and Drop) (2) | 2023.12.20 |
외부파일 드래그 앤 드롭1(File Drag and Drop) (3) | 2023.12.18 |
Form(폼) Serialize 속 살짝 들여다 보깅! (2) | 2023.11.24 |
이벤트 동적 바인딩 당신만 몰라용!! (2) | 2023.11.23 |