Dropzone.js 소개
Dropzone.js는 프론트 페이지에서 사용하는 드래그드롭 라이브러리이다.
아래와 같이 생겼다.
Backend에서는 Dropzone을 통해 받은 파일을 처리해서 DB에 저장하면 된다.
알아두어야 하는 것들
Dropzone을 구현하면서 알아두어야 할 것
- Dropzone의 기본설정은 파일을 올리는 순간 서버로 보낸다. 이를 방지하기 위해 옵션을 설정해 주어야 한다.
- Dropzone에서 여러 개 파일을 동시에 올리는 것은 기본 값이 아니다.
- Dropzone에서 바로 보내므로 header 값을 설정해주어야 한다. 특히 jwt를 사용할 경우에는 Bearer 토큰을 header에 붙여주어야 한다.
- 만약 추가로 formData에 값을 집어넣어 req.body에서 사용하고 싶다면 메소드 안에 따로 append해서 저장해 두어야 한다.
- 중복된 파일을 올리지 못하게 막으려면 따로 함수를 붙여주어야 한다.
- **새로운 파일을 추가할때만 파일을 보낼 수 있으므로, 만약 기존에 있던 파일이 남아있고 업데이트를 해주어야 하는 상황이라면 다른 방식으로 파일을 보내주어야 한다.
<form action="" class="dropzone" method="post" enctype="multipart/form-data" id="dragUpload">
<div class="fallback">
<input name="file" name="files" multiple />
</div>
</form>
html에 Dropzone form을 만들어주고 method와 enctype을 설정해준다.
var myDropzone = new Dropzone(".dropzone", {
url: 'http://localhost:8080/card/upload',
method: "post",
autoProcessQueue: false, //자동으로 보내기 방지
paramName: 'files',
parallelUploads: 99,
maxFilesize: 10, // MB
uploadMultiple: true,
headers: {
"Authorization": 'Bearer ' + token //localstorage에 저장된 token
},
});
그리고 Dropzone 인스턴스를 생성해서 사용해 주면 되는데,
autoProcessQueue에 false를 붙여주지 않으면 자동으로 진행된다.
parallelUploads에는 여러 개를 동시에 보내는 옵션인데 기본값이 2로 되어있으므로 여러개를 보내고 싶다면 임의의 값을 지정해주어야 한다.
init: function() {
var myDropzone = this;
this.on("sending", function(file, xhr, formData) {
let cardName = $("input[name='cardname']").val();
formData.append("name", cardName);
});
this.on("queuecomplete", function(file) {
document.location.href = "./cardnews.html" //업로드가 완료되었다면 이전 화면으로 이동
});
this.on("addedfile", function(file) { //중복된 파일의 제거
if (this.files.length) {
// -1 to exclude current file
var hasFile = false;
for (var i = 0; i < this.files.length - 1; i++) {
if (this.files[i].name === file.name && this.files[i].size === file.size &&
this.files[i].lastModifiedDate.toString() === file.lastModifiedDate.toString()) {
hasFile = true;
this.removeFile(file);
}
}
if (!hasFile) {
addedFiles.push(file);
}
} else {
addedFiles.push(file);
}
});
$('#btn_confirm').click(function(e) {
let cardName = $("input[name='cardname']").val();
if (cardName.length == 0) {
alert('카드제목 입력 필요')
return;
}
if (myDropzone.getRejectedFiles().length > 0) {
let files = myDropzone.getRejectedFiles();
alert('거부된 파일이 있습니다.', files)
return;
}
e.preventDefault();
e.stopPropagation();
myDropzone.processQueue();
})
}
위에서 보는 것처럼 #btn_confirm을 통해 저장을 눌러서 myDropzone.processQueue();
를 호출해주어야 dropzone 라이브러리가 동작한다.
올리고 카드를 수정할 시
이 부분이 참 고민을 많이 했던 부분.
처음 카드를 올릴때 Dropzone으로 파일을 보내기가 가능했다고 하지만, 카드를 수정할 때의 경우에는 다르게 접근이 필요하다.
위에서 보듯이 어떤 파일은 추가가 될 것이고 어떤 파일은 삭제가 된다. 어떨때는 순서만 바뀔 수도 있다. 그렇기 때문에 새로운 파일을 추가할 때만 사용할 수 있는 Dropzone 라이브러리의 sending기능은 사용할 수 없다. 따라서 xhr로 파일을 직접 보내야 한다.
- 페이지를 열었을 때 API를 통해 가져오는 첫 데이터는 sort_row에서 추출한다.
- 모든 작업이 끝나고 저장 버튼을 누를 때 sort_row에서 값들을 추출한다.
- 백엔드에서는 값들이 온전하게 다 들어가 있는 경우에는 손대지 않고
- 값이 없는 애들 (filename, path)가 없는 경우는 xhr로 받아온 파일에서 값을 매핑해준다.
<li class="sort_row sort_row${idx}" data-filename="${subCard[idx].SUBCARD_FILENAME}">
<div class="sortNumber">${subCard[idx].SUBCARD_ORDER}</div>
<div><img src="images/draggable.png" alt="draggable icon"></div>
<div class="thumbnail" style="background-image: url('http://localhost:8080/${fileURL}')"></div>
<div class="img_zoom">
<p>이미지 크게보기</p>
<div class="zoom">
<img src="http://localhost:8080/${fileURL}">
</div>
</div>
<div class="file_name" id="${subCard[idx].SUBCARD_FILEORIGIN_NAME}">${subCard[idx].SUBCARD_FILEORIGIN_NAME}</div>
<div class="item_delete">
<img src="images/delete-forever.png" alt="delete">
</div>
</li>
즉 시작하게 되면 저 sort row에서 API를 붙여 만들게 되는데, 수정이 모두 끝난 후 data를 백엔드로 보내 처리할 때 저장 당시의 sort row에서 값을 가져와 보내면 된다.
let fileArr = [];
let sortRow = document.querySelectorAll('.sort_row');
sortRow.forEach(el => {
fileArr.push({
originalname: $(el).children('.file_name').text(),
order: $(el).children('.sortNumber').text(),
path: 'uploads/' + $(el).data('filename'), //없으면 undefined
filename: $(el).data('filename') //없으면 undefined
});
});
이제 이 값 fileArr과 dropzone을 통해 받아온 newFiles을 xhr로 보내주면 된다.
/**
* @param {String} cardName : 카드제목
* @param {array} fileArr : 저장당시의 카드 데이터들
* @param {array} newFiles : Dropzone을 통해 추가한 파일들
*/
const updateCardNews = async (cardName, fileArr, newFiles) => {
let urlIDX = URLParser();
var formData = new FormData();
let xhr = new XMLHttpRequest();
xhr.open('post', `${URL}/card/${urlIDX}`);
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
// xhr.setRequestHeader('Content-Type', 'multipart form-data'); 붙이면 안간다. 왜지?
let jsonfiles = JSON.stringify(fileArr);
let jsonName = JSON.stringify(cardName);
formData.append('fileArr', jsonfiles);
formData.append('cardName', jsonName);
for (let index in newFiles) {
formData.append('files', newFiles[index]);
}
xhr.send(formData);
xhr.onreadystatechange = function() {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200 || xhr.status === 201) {
window.location.href = 'cardnews.html';
} else {
console.error("card Update 결과값을 받아오지 못함", xhr.response);
}
}
};
};
파일을 이렇게 보내준다. 헷갈렸던 것은 21번 라인 부분인데, newFiles를 File 타입으로 받아오더라도 각각의 파일을 formData에 appand 시켜줘야 한다.
xhr.setRequestHeader('Content-Type', 'multipart form-data');
를 xhr에 붙이게 되면 값이 들어가지 않는다. 이유가 뭘까?
서버측 처리 방법
애먹었었던 카드를 업데이트 하는 부분이다.
let fileArr = req.body.fileArr;
let newFiles = req.files;
fileArr = JSON.parse(fileArr);
fileArr.forEach(el => {
if (el.filename === undefined) {
newFiles.forEach(file => {
if (el.originalname === file.originalname) {
el.filename = file.filename;
el.path = file.path;
}
});
}
});
보면 fileArr부분에서 코드 처리 부분이다.
우리가 받은 파일 중 새로올린 파일은 filename과 path가 없는 상태이다.
- 최종 파일 데이터인 fileArr을 돌면서 배열에 filename이 있는지 확인한다.
- 파일이 undefined로 되어 있다면?
- 새로 받아온 newFiles에서 originalName이 같은 것을 확인한다.
- 없는 엘리먼트에 newFile의 이름과 path를 넣어준다.