PHP & JavaScript Drag and Drop 구현 (Dropzone 사용)

Dropzone에 대한 심화 정보는 국내에 많이 없어서 디버깅이 진짜 어려웠다…

잊기 전에 이미지 Drag and Drop 업로드 및 수정, 삭제, 다운로드 모두 구현하는 코드에 대하여 설명하겠다.


1. DB생성 및 설정

설명을 위해 다음과 같이 간단한 DB를 생성해 구현했지만 실제로 사용할 것이라면 컬럼만 추가해도 무방하다.

CREATE TABLE `imgs` (
	`img` VARCHAR(300) NULL DEFAULT NULL COLLATE 'utf8_general_ci'
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

2. jQuery 및 Dropzone 스크립트 태그 추가

파일 최상단 또는 Head 파일에 다음과 같이 작성하여 jQuery와 Dropzone을 사용하기 위해 추가한다.

<script src="https://code.jquery.com/jquery-3.6.1.js" integrity="sha256-3zlB5s2uwoUzrXK3BT7AX3FyvojsraNFxCc2vC/7pNI=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" type="text/css"/>

3. 업로드 버튼 및 폼 작성

DB에 이미지 데이터가 없다면 이미지 등록 버튼을 클릭 시 이미지 등록 폼을 띄우고 등록 폼에서 업로드하려는 이미지의 미리 보기를 확인 후 등록 폼의 등록 버튼으로 업로드가 가능하고, 반대로 이미지 데이터가 있다면 이미지 등록 버튼 대신 이미지 미리 보기가 보이며 미리 보기 클릭 시 이미지 수정 폼을 띄워 수정할 수 있게 구현한다.

폼의 클래스를 dropzone으로 설정하여 Dropzone영역을 생성하고, 폼 데이터가 서버로 전송될 때 해당 데이터가 인코딩되는 방법을 명시하는 enctype 속성의 값을 모든 문자를 인코딩하지 않음을 의미하는 multipart/form-data로 설정한다. 이 속성은 보통 파일이나 이미지를 서버로 전송할 때 주로 사용한다.

파일 업로드를 위해 input 태그의 type은 file로 설정하고 속성값은 파일을 다중 선택할 수 있도록 multiple로 설정한다.

<?php
    global $mysqli;

    $sql = "select * from imgs";
    $result = $mysqli->query($sql);
    if($result){
        $row = $result->fetch_array();
    }

    if(!empty($row)){ // 데이터가 있다면 미리 보기 없다면 등록 버튼 표시
        $imgArr = explode("|", $row["img"]); // 이미지 이름 데이터는 구분자 |로 구분
        $imgScript = "<a herf='#' class='img_mod text-decoration-none'>";
        for($s=0; $s<count($imgArr)-1; $s++){
            $imgScript .= "<img class='simgs' src='/uploads/".$imgArr[$s]."'>&nbsp;";
        }
        $row["img"] = $imgScript."</a>";
    }else{
        $row["img"] = "<button type='button' class='img_add'>이미지 등록</button>";
    }

    echo $row["img"];
?>
<div id="img_frm">
	<h4>첨부 사진(최대 3개)</h4><br>
    <form class="dropzone" id="dropzone-file" enctype="multipart/form-data" method="POST" style="display:none; z-index: 999; text-align:center;"> <!-- enctype 설정 필수 -->
        <div class="dz-message needsclick">
            <b class="note needsclick">클릭 또는 이미지를 드래그하세요.</b>
	    </div>
        <div class="fallback">
            <input name="file" type="file" multiple> <!-- multiple 필수 -->
        </div>
    </form><br>
	<button type="button" id="btn_upload_file">등록</button>
	<button type="button" id="btn_upload_back">닫기</button>
</div>

<script type="text/javascript" src="동작 구현 파일 경로"></script>

4. 동작 구현 파일 작성

먼저 공식 문서에 명시된 대로 Dropzone.autoDiscover를 false로 선언하여 Dropzone을 초기화한다. id로 Dropzone의 생성 여부를 확인하고 Dropzone 영역이 성공적으로 생성되었다면 아래 코드와 같은 옵션으로 Dropzone을 선언한다.

자동 업로드 옵션을 사용하지 않으므로 등록 폼에 올라간 이미지를 미리 보고 등록 폼의 등록 버튼을 클릭하면 선언한 Dropzone의 업로드 프로세스를 processQueue()로 호출하여 실행한다.

이후 업로드된 이미지 데이터가 있다면 등록 폼은 수정 폼으로 변경되며, 이미지를 경로 데이터로 불러와서 수정 폼에 출력한다. 이 과정에서 이미지의 이름, 용량, 최대 업로드 가능 개수를 알맞게 불러오고 전체 이미지 다운로드 버튼이 활성화될 수 있게 구현한다.

Dropzone.autoDiscover = false; // Dropzone 초기화

$(function(){
    if($("#dropzone-file").length){
        var myDropzone = new Dropzone("#dropzone-file", { 
        url : "업로드 처리 파일 경로",
        autoProcessQueue: false, // 자동 업로드 여부(true 자동 업로드, false processQueue() 호출 시 업로드)
        clickable: true, // 클릭 가능 여부
        maxFiles: 3, // 최대 업로드 가능 개수
        maxFilesize: 10, // 최대 업로드 용량(MB)
        parallelUploads: 3, // 동시 파일 업로드 수
        addRemoveLinks: true, // 삭제 버튼 표시 여부
        dictRemoveFile: "삭제", // 삭제 버튼 표시 텍스트
        uploadMultiple: true, // 다중 업로드 기능
        paramName: "file",
        acceptedFiles: ".png,.jpg,.gif", // 허용 업로드 파일 형식
        dictInvalidFileType: "사용할 수 없는 형식의 파일입니다.",
        dictMaxFilesExceeded: "최대 3개까지 업로드 하실 수 있습니다.",
        removedfile: function(file){ // 삭제 버튼 클릭 시 수행할 동작 설정
            var fileName = file.name;
            var maxFiles = myDropzone.options.maxFiles;
            if(maxFiles >= 0){
                $.ajax({
                type: "POST",
                url: "삭제 처리 파일 경로",
                data: {"name": fileName},
                success: function(data){
                    myDropzone.options.maxFiles += 1; // 삭제되었다면 업로드 가능한 파일의 수를 더한다
                }
                });
            }
            var _ref;
            return (_ref = file.previewElement) != null ? _ref.parentNode.removeChild(file.previewElement) : void 0;
        }
        });
    }

    $("#btn_upload_file").click(function(){
        myDropzone.processQueue(); // 버튼 클릭 시 파일 업로드 실행
    });

    $(".img_mod").on("click", function(e){
        $("#img_frm").show();
        var imgs = [];
        var imgsArr = "";
        imgs = $(e.currentTarget).find(".simgs");
        for(var i=0; i<imgs.length; i++){
        var imgname = decodeURI(imgs[i]["src"]).split("/"); // 수정할 이미지 이름 추출
            (function(i){
                console.log(imgname.at(-1));
                $.ajax({
                    url: "이미지 용량 체크 파일 경로", 
                    type: "POST", 
                    data: "imgname=" + imgname.at(-1),
                    success: function(data){
                    data = JSON.parse(data);
                    if(data){
                        var imgsize = data.filesize;
                        var mockFile = {name: data.filename, size: imgsize};
                        myDropzone.displayExistingFile(mockFile, imgs[i]["src"]);
                        myDropzone.files.push(mockFile); // 체크한 이미지 용량 데이터를 Dropzone으로 전송
                    }
                    }
                });
            })(i);
            imgsArr = imgsArr + imgname.at(-1)+"|";
        }
        $("#btn_upload_file").before("<button type='button' class='btn btn-success' id='download_all' data-imgs='"+imgsArr+"'>전체 다운로드</button> "); // 수정 폼에서는 전체 다운로드 버튼 표시
        myDropzone.options.maxFiles = 3 - i;
    });
});

$(".img_add").on("click", function(e){
    $("#img_frm").show();
});

$("#btn_upload_back").click(function(){
    $("body").load("이미지 업로드 폼 페이지 경로"); // 닫기 시 페이지 초기화
    $("#img_frm").hide();
});

function download_file(filename, filepath){ // 이미지 다운로드 함수
    var element = document.createElement("a");
    element.setAttribute("href",filepath);
    element.setAttribute("download", filename);
    document.body.appendChild(element);
    element.click();
  }

$(document).on("click", "#download_all", function(){
    var imgsArr = $(this).attr("data-imgs");
    imgsArr = imgsArr.split("|");
    for(var i=0; i<imgsArr.length-1; i++){
        download_file(imgsArr[i], "이미지 저장 폴더 경로"+imgsArr[i]);
    }
});

버튼과 폼이 동작하는지 확인한다.
디자인은 간단하게 부트스트랩을 사용했다.

5. 업로드, 수정, 삭제 처리 파일 작성

이미지 저장 폴더가 없다면 이미지 저장 폴더를 생성하고, POST로 받아온 이미지 데이터를 임시 데이터로 가지고 있다가 같은 이름의 이미지가 동시에 올라갈 때 중복을 방지하기 위해 이름 앞에 날짜 데이터를 덧붙여서 새로운 이름으로 이미지를 서버에 저장한다.

이미지를 저장할 때 이전에 이미지 데이터가 없었다면 | 로 구분하여 이미지 파일의 경로를 DB에 저장하고, 이미지 데이터가 있었다면 | 를 기준으로 데이터를 덧붙여 DB에 저장한다.

이미지를 삭제할 때에는 삭제하고 남은 이미지 데이터가 없다면 DB에서 이미지 파일 데이터를 모두 삭제하고, 남은 이미지 데이터가 있다면 삭제한 이미지 파일의 경로만 DB에서 | 로 구분하여 찾아낸 후 부분적으로 삭제한다. 이후 서버에서도 삭제된 이미지 파일의 경로를 찾아서 삭제한다.

<?php
    global $mysqli;
    $name = $_POST["name"];
    $upload_folder = $_SERVER["DOCUMENT_ROOT"]."이미지 저장 폴더 경로";

    if(!empty($_FILES)){
        if(!file_exists($upload_folder) && !is_dir($upload_folder)) mkdir($upload_folder); // 이미지 저장 폴더가 없다면 생성
        $fileInfo = "";
        foreach($_FILES["file"]["tmp_name"] as $key => $value){
            $tempFile = $_FILES["file"]["tmp_name"][$key];
            $targetFile = $upload_folder."/".date("YmdHis").$_FILES["file"]["name"][$key]; // 서버에 이미지 저장 시 이미지 이름 앞에 날짜 데이터를 붙여서 중복 방지
            if(move_uploaded_file($tempFile, $targetFile)){
                $fileInfo .= date("YmdHis").$_FILES["file"]["name"][$key]."|";
            }
        }

        $osql = "select * from imgs";
        $result = $mysqli->query($osql);
        $row = $result->fetch_array(); 

        if($row){ // 이전에 이미지 데이터가 있었다면 덧붙여 수정
            $sql = "update imgs set img = '".$row["img"].$fileInfo."'";
        }else{
            $sql = "insert into imgs (img) values ('".$fileInfo."')";
        }
        $mysqli->query($sql);
    }

    if(!empty($name)){ 
        $osql = "select * from imgs";
        $result = $mysqli->query($osql);
        $row = $result->fetch_array();
        $new_fileInfo = str_replace($name.'|','',$row["img"]);

        if($new_fileInfo==""){ // 삭제 시 남은 이미지 데이터가 있다면 데이터 부분 삭제 
            $sql = "delete from imgs";
        }else{
            $sql = "update imgs set img = '".$new_fileInfo."'";
        }
        $mysqli->query($sql);

        unlink($upload_folder."/".$name);
    }
?>

6. 이미지 용량 체크 파일 작성

PHP 내장 함수 filesize()로 이미지 용량을 구해서 각 이미지의 용량 데이터를 JSON 형식으로 전송한다. JSON형식으로 전송하기 위해 header 설정은 필수로 확인해야 한다.

<?php
    header("Content-Type: application/json");

    $imgname = $_POST["imgname"];
    $upload_folder = $_SERVER["DOCUMENT_ROOT"]."이미지 저장 폴더 경로";

    $imgSize = filesize($upload_folder."/".$imgname);
    $imgSize = floor($imgSize);  

    echo(json_encode(array("filesize" => $imgSize, "filename" => $imgname)));
?>

Drag and Drop 업로드 처리가 잘 되는지 확인한다.

수정 폼 동작 확인, 용량 표시 확인, 삭제 후 업로드 처리가 다시 잘 되는지 전부 확인한다.

전체 이미지 다운로드가 잘 되는지 확인한다.

move_uploaded_file(string $from, string $to);
지정된 파일이 유효한 업로드 파일인지 확인하고 유효하면 지정한 파일명으로 이동한다.

unlink(string $filename, ?resource $context = null);
지정한 경로의 파일을 삭제한다.

filesize(string $filename);
지정한 경로의 파일 크기를 불러온다.

floor(int|float $num);
소수점을 버리고 정수로 변환한다.


위로 스크롤