본문 바로가기

Develop/Front-End

dropzone.js 를 이용하여 동영상 업로드 미리보기 및 썸네일 기능 구현기

이번에 내가 맡게 된 업무는 dropzone.js 를 이용하여 동영상 업로드와 미리보기 및 썸네일 기능을 만드는 것이였다.

요즘들어 부쩍 스크립트 만질 일이 많아졌는데

BE 코드를 다루지 못하고 있어서 아쉬움이 많다... 😭

 

 

업무 진행 당시 dropzone.js version : 5.5.0


 

진행상황

 

동영상 업로드 , 동영상 특정 구간 썸네일 추출 , 미리보기 기능 구현 완료! 😎


이슈 & 고민 및 해결

 

배너배리에이션 기능을 만들었던 이미지 등록하는 화면에서

당시에는 발견하지 못했던 버그들도 발견하면서 수정하고 dropzone.js 오픈 소스 라이브러리 코어 스크립트를 조금

수정하였다. 😂

 

이 업무를 진행하면서 이슈나 고민 거리는 없었던 것 같다 🤗

 

[지난 배너 배레이이션 작업 이후 체크 못한 결함 사항 수정 리스트 🛠]

1. 250*250 소재 업로드 후 250*250 소재 다시 업로드 하면 중복 로직에 막힘
   이후 250*250 소재 삭제 하고 다시 업로드 하기 전까지 이미지 자동생성 안되는 문제 
   -> 자동생성 유효성 로직 수정 ( 사용 할 수 있는 객체 값을 다시 찾아 할당하는 방식으로 수정)
2. 미리보기 표출 좌표 계산식 수정 ( 표출하는 영역 높이에 따라 미리보기 이미지들이 잘려서 안보일 수 있으므로 계산식을 통해 표출하는 위치를 조정)
  -> 페이지 레이아웃이 변경 되어 잘못 계산 되는 수식 수정
  as-is  preveiw element y 좌표 + preveiw 높이 > 상위 element 높이
  to-be preview 높이 + preview 바닥 좌표 +  이미지 높이 + 이미지 바닥 좌표  > 상위 element 높이

3. 기존 홀로그램 배너 수정 시 데이터 값 표출 누락 수정

[dropzone core script 동영상 업로드 가능하도록 커스터마이징]
*** el로 유효성 모두 판단 ***
1. addedfile 이벤트 중 영상 파일 이름으로 업로드 할 동영상 규격 맞는지 여부 판단
1-1. el 생성 및 이벤트
2. dropzone.js videoUploadSucess, videoUploadFail 이벤트 리스너 추가 ( 동영상 업로드 이후 무해화 로직 , 실패 시 화면에 처리할 로직 추가 )
3. 썸네일 추출 ( 영상 특정 구간 캔버스로 변환 후 사진 추출 ) - 지난 업무 중 배너 배리에이션 기능을 만들면서

캔버스에 조금 익숙해져서 바로 생각나서 기능을 만들었던 것 같다, 😁 (캔버스 API 도전기)


참고 레퍼런스

https://www.dropzone.dev/

 

Dropzone.js

Dropzone.js is an open source library that provides beautiful and easy to use drag'n'drop file uploads with image previews.

www.dropzone.dev

 

https://github.com/dropzone/dropzone

 

GitHub - dropzone/dropzone: Dropzone is an easy to use drag'n'drop library. It supports image previews and shows nice progress b

Dropzone is an easy to use drag'n'drop library. It supports image previews and shows nice progress bars. - GitHub - dropzone/dropzone: Dropzone is an easy to use drag'n'drop library...

github.com

 

https://developer.mozilla.org/ko/docs/Web/JavaScript

 

JavaScript | MDN

JavaScript (JS)는 가벼운, 인터프리터 혹은 just-in-time 컴파일 프로그래밍 언어로, 일급 함수를 지원합니다. 웹 페이지를 위한 스크립트 언어로 잘 알려져 있지만, Node.js, Apache CouchDB, Adobe Acrobat처럼

developer.mozilla.org


 

TO DO List

1. 단위테스트

2. 코드 리팩토링 (불필요 코드 제거 개발 중 디버깅 콘솔 로그 제거,변수명 정리)(FE,BE)


Example Code

dropzone.js core 파일 추가 이벤트에 파일 타입이 영상 일 때 처리

내부 이벤트 리스너에 "videoUploadSucess","videoUploadFail" 이벤트 추가

this.prototype.events = ["drop", "dragstart", "dragend", "dragenter", "dragover", "dragleave", "addedfile", "addedfiles", "removedfile", "thumbnail", "error", "errormultiple", "processing", "processingmultiple", "uploadprogress", "totaluploadprogress", "sending", "sendingmultiple", "success", "successmultiple", "canceled", "canceledmultiple", "complete", "completemultiple", "reset", "maxfilesexceeded", "maxfilesreached", "queuecomplete","videoUploadSucess","videoUploadFail"];
....
....
....
addedfile: function addedfile(file) {
                    console.log("process core");
                    var _this2 = this;

                    if (this.element === this.previewsContainer) {
                        this.element.classList.add("dz-started");
                    }

                    if (this.previewsContainer) {
                        file.previewElement = Dropzone.createElement(this.options.previewTemplate.trim());
                        file.previewTemplate = file.previewElement; // Backwards compatibility
                        if (this.options.previewArea === null) {
                            if (file.type === "video/mp4"){
                                var _obj = {"title" : "" , "code" : "" ,"titleEng" : "" , "imageSize" : ""};
                                var vidUrl = window.URL.createObjectURL(file);

                               if(file.name === "width.mp4"){
                                    _obj["title"] = "가로형";
                                    _obj["code"] = "96";
                                    _obj["titleEng"] = "width";
                                    _obj["imageSize"] = "VIDEO_WIDTH";
                                }else if(file.name === "length.mp4"){
                                    _obj["title"] = "세로형";
                                    _obj["code"] = "97";
                                    _obj["titleEng"] = "length";
                                    _obj["imageSize"] = "VIDEO_HEIGHT";
                                }else if(file.name === "square.mp4"){
                                    _obj["title"] = "사각형";
                                    _obj["code"] = "98";
                                    _obj["titleEng"] = "square";
                                    _obj["imageSize"] = "VIDEO_SQUARE";
                                }

                                if($(`#div_${_obj["titleEng"]}`).length > 0){
                                    return this.emit("videoUploadFail", file , "동일파일 이슈");
                                    return false;
                                }else if(file.size > 5242880){
                                    return this.emit("videoUploadFail", file , "용량 이슈");
                                    return false;
                                }

                                var _div = document.createElement('div');
                                    _div.className = "imgItem";
                                    _div.id = `div_${_obj["titleEng"]}`;
                                    _div.dataset.title = _obj["title"];
                                    _div.dataset.code = _obj["code"];
                                    _div.dataset.titleEng = _obj["titleEng"];
                                    _div.dataset.imageSize = _obj["imageSize"];

                                    _div.innerHTML = `<button type="button" class="ico-close-gray" onclick="deleteVideoItem(this)" ></button>
                                                      	<div class="videoBox">
                                                      		<video autoplay="" controls="" loop="" muted="" width="181px" height="120px">
                                                      			<source src=${vidUrl}>
                                                      		</video>
                                                      	</div>
                                                      	<div class="textBox">
                                                      		<em>[PC/M]</em>
                                                      		<p>${_obj["title"]}</p>
                                                      		<button type="button" class="btn-ss btn-gray-line" onclick="previewVideoItem(this)">미리보기</button>
                                                      	</div>`;

                                if(file.name !== "width.mp4" && file.name !== "length.mp4" && file.name !== "square.mp4"){
                                    return this.emit("videoUploadFail", file , "파일명 이슈");
                                    return false;
                                }

                                document.getElementById("mobonVideoUpload").appendChild(_div);
                                file.previewElement = _div;
                                return this.emit("videoUploadSucess", file);
                            }else{
                                document.getElementById("mobonImageUpload").appendChild(file.previewElement);
                            }
                        } else {
                            this.options.previewArea.appendChild(file.previewElement);
                        }
                        for (var _iterator3 = file.previewElement.querySelectorAll("[data-dz-name]"), _isArray3 = true, _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) {
                            var _ref3;

                            if (_isArray3) {
                                if (_i3 >= _iterator3.length) break;
                                _ref3 = _iterator3[_i3++];
                            } else {
                                _i3 = _iterator3.next();
                                if (_i3.done) break;
                                _ref3 = _i3.value;
                            }

                            var node = _ref3;

                            node.textContent = file.name;
                        }
                        for (var _iterator4 = file.previewElement.querySelectorAll("[data-dz-size]"), _isArray4 = true, _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) {
                            if (_isArray4) {
                                if (_i4 >= _iterator4.length) break;
                                node = _iterator4[_i4++];
                            } else {
                                _i4 = _iterator4.next();
                                if (_i4.done) break;
                                node = _i4.value;
                            }

                            node.innerHTML = this.filesize(file.size);
                        }

                        if (this.options.addRemoveLinks) {
                            //file._removeLink = Dropzone.createElement("<a class=\"dz-remove\" href=\"javascript:undefined;\" data-dz-remove>" + this.options.dictRemoveFile + "</a>");
							file._removeLink = Dropzone.createElement("<a class=\"dz-remove btn-del\" href=\"javascript:undefined;\" class=\"btn-del\" data-dz-remove>삭제</a>");
                            file.previewElement.appendChild(file._removeLink);
                        }

                        var removeFileEvent = function removeFileEvent(e) {
                            e.preventDefault();
                            e.stopPropagation();
                            if (file.status === Dropzone.UPLOADING) {
                                return Dropzone.confirm(_this2.options.dictCancelUploadConfirmation, function () {
                                    return _this2.removeFile(file);
                                });
                            } else {
                                if (_this2.options.dictRemoveFileConfirmation) {
                                    return Dropzone.confirm(_this2.options.dictRemoveFileConfirmation, function () {
                                        return _this2.removeFile(file);
                                    });
                                } else {
                                    return _this2.removeFile(file);
                                }
                            }
                        };

                        for (var _iterator5 = file.previewElement.querySelectorAll("[data-dz-remove]"), _isArray5 = true, _i5 = 0, _iterator5 = _isArray5 ? _iterator5 : _iterator5[Symbol.iterator]();;) {
                            var _ref4;

                            if (_isArray5) {
                                if (_i5 >= _iterator5.length) break;
                                _ref4 = _iterator5[_i5++];
                            } else {
                                _i5 = _iterator5.next();
                                if (_i5.done) break;
                                _ref4 = _i5.value;
                            }

                            var removeLink = _ref4;

                            removeLink.addEventListener("click", removeFileEvent);
                        }
                    }
                }
...
...
...
videoUploadSucess: function videoUploadSucess(file){},
videoUploadFail: function videoUploadFail(file,errMsg){},

...
...
...
function deleteVideoItem(element) {
    element.parentElement.remove();
}

function previewVideoItem(element){
 var videoSrc = $(element).parent().siblings('.videoBox').find("video > source").attr('src');
 $(element).parent().parent().append("<div class='videoWrap'><video class='model-vid' autoplay controls loop muted width='100%' height='100%'><source src='" + videoSrc + "' type='video/mp4'></source></video></div>");
}


 
동영상 썸네일 

클라이언트 단에서 동영상 썸네일 추출해야 했기에

캔버스 API를 이용해 동영상 특정 구간을 캔버스에 그린 후 base64 로 인코딩하여 작업하였다. 😛

         this.on("videoUploadFail", function (file,errMsg) {
                    console.debug("videoUploadFail", file);
                    alert("업로드 파일을 다시 한 번 확인해 주세요. \n가로형 : width.mp4\n세로형 : length.mp4\n사각형 : square.mp4");

                    var video = document.createElement("video");
                        video.style.width = "80px";
                        video.style.height = "80px";

                    var source = document.createElement("source");
                        source.src = window.URL.createObjectURL(file);

                    video.appendChild(source);

                    var thumbnailCanvas = document.createElement("canvas");
                        thumbnailCanvas.width = 180;
                        thumbnailCanvas.height = 180;

                    var ctx = thumbnailCanvas.getContext("2d");

                    // 'loadedmetadata' 이벤트를 등록하고, 이벤트가 한 번만 실행되도록 함
                    video.addEventListener("loadedmetadata", function () {
                        video.currentTime = 1; // 원하는 시간(초)으로 이동
                    }, { once: true });

                    video.addEventListener("seeked", function () {
                        ctx.drawImage(video, 0, 0, thumbnailCanvas.width, thumbnailCanvas.height);
                        var thumbnailURL = thumbnailCanvas.toDataURL();

                        var failEl = document.createElement("div");
                        failEl.className = "imgItem noimg";
                        //<button type="button" class="ico-none ico-close-gray"></button>
                        failEl.innerHTML = `<img src="${thumbnailURL}" width="180px" height="180px">
                                                <div class="textBox">
                                                    <p style="overflow-x: hidden;">${file.name}</p>
                                                </div>
                                                <div class="errorLabel error_type01">
                                                    <i></i><span>${errMsg}</span>
                                                </div>
                                            `;
                        document.getElementById("videoFailContainer").innerHTML="";
                        document.getElementById("videoFailContainer").appendChild(failEl);
                        myDropzone.removeFile(file);
                    }, { once: true });
                    video.load();
                });



 

반응형