// DinoWorks Form Script 2020-06-01
var docFileInfoPrev = [];
var docFileInfo = [];
var docFormInfo = [];

// Form 정보 제출(File 제외)
$.fn.ajaxSendFormData = function(options) {
    var settings = $.extend({
        formName: '',                             // 전송할 데이터가 있는 form의 name 속성값
        functionUrl: '',                          // 전송 데이터를 처리할 페이지
        data: {},                                 // 전송할 데이터 ({formname: value, formname: value})
        moveUrl: '',                              // 전송후 이동할 페이지 (값이 있는 경우만 동작 / 혹은 backend 에서 data.moveUrl로 전달)
        confirmMsg: '',                           // Confirm Box 표시 여부 (메세지가 있으면 문의 / 없으면 바로 실행)
        useAjax: true,                            // 미사용시(false) 일반 submit 형태로 동작
        checkValidation: false,                   // beforesend 시점에 유효성 검사 여부
        callbackBefore: function() {},            // callback 함수 지정(before send)
        callbackSuccess: function() {},           // callback 함수 지정(success)
        callbackError: function() {},             // callback 함수 지정(Error)
        callbackComplete: function() {}           // callback 함수 지정(complete)
    }, options);

    // 유효성 검사
    if (this.length !== 1) {    // 대상은 반드시 1개만 지정
        //alert('submit 버튼으로 지정된 대상이 없거나 2개 이상입니다. (1개만 지정해야 함)');
        return false;
    }
    if (settings.functionUrl === '') {    // 데이터를 처리할 파일 지정(필수)
        alert('functionUrl (프로그램 파일)이 정의되지 않았습니다.');
        return false;
    }
    if (settings.formName !== '' && this.closest('form').length !== 1) {     // 데이터를 포함하는 form 태그 지정여부 확인
        alert('submit 대상이 되는 form 태그가 지정되지 않았습니다.');
        return false;
    }

    this.on('click', function() {
        if (settings.confirmMsg !== '') {
            if (!confirm(settings.confirmMsg)) return false;
        }
        if (settings.useAjax === false) {     // ajax option 제외하고 바로 사용 가능
            $(this).closest('form').attr({'action': settings.functionUrl, 'method': 'post', 'enctype': 'multipart/form-data'});
            $(this).closest('form').trigger('submit');
            return false;
        } else {
            // submit 동작 제거 (혹시 button input 인 경우에 submit 될 수 있음)
            $(this).closest('form').on('submit', function(e) {
                e.preventDefault();
            });
        }

        // 전송할 데이터 준비
        var $form = (settings.formName === '') ? $(this).closest('form') : $('form[name="' + settings.formName + '"]');
        var formData = new FormData();
        var inputData = $form.serializeArray();    // file은 포함이 안됨 [{name: "seq", value: "23"}, {...}] 형태
        var getData = '';                          // 조회된 결과
        var digitOrigin = [];
        var modHistory = '';                       // 수정이력 정리

        // 천단위 숫자 정리(콤마 제외한 원래 숫자 정리)
        $form.find('input[data-type="int-plus"], input[data-type="int"], input[data-type="real-plus"], input[data-type="real"]').each(function(i) {
            digitOrigin[i] = {name: $(this).attr('name'), value: $(this).val().replace(/,/g, '')}
        });
        // 사용자가 입력한 정보 준비
        for (var key in settings.data) {
            formData.append(key, settings.data[key]);
        }
        // 해당 form의 전달 정보 정리
        for (var i = 0; i < inputData.length; i++) {
            // 천단위 콤마 제외한 정보로 변경
            for (var j = 0; j < digitOrigin.length; j++) {
                if (inputData[i].name === digitOrigin[j].name) {  // 이름이 일치하면
                    inputData[i].value = digitOrigin[j].value;
                    digitOrigin.splice(j, 1);    //  해당 원소 배열에서 삭제 ([], 동일 name이 많이 있을 수 있음. 앞부터 매칭)
                    //console.log(digitOrigin);
                    break;
                }
            }
            formData.append(inputData[i].name, inputData[i].value.trim());    // input 정보 입력(trim 적용)
        }
        // 해당 form의 전달 정보 정리(파일 / 신규첨부 내용만)
        for (var obj in docFileInfo) {
            for (var i = 0; i < docFileInfo[obj].length; i++) {
                // canvas에서 조정(resize, rotate 된 경우 filename file명이 file 객체에 미지정됨 / Blob 으로 보임 --> 직접명시 필요)
                if (docFileInfo[obj][i].type = 'image' && typeof(docFileInfo[obj][i].file) === 'object') {
                    formData.append(obj + '[]', docFileInfo[obj][i].file, docFileInfo[obj][i].name);
                } else {
                    formData.append(obj + '[]', docFileInfo[obj][i].file);
                }
            }
        }


        // 수정이력 확인 및 첨부
        // console.log(inputData);
        // console.log(docFormInfo);
        var indexName = [];
        var modHistory = '';
        for (var i = 0; i < inputData.length; i++) {
            for (var key in docFormInfo) {
                if (indexName[key] === undefined) indexName[key] = 0;
                if (key === inputData[i].name) {
                    var dataPrev = '';
                    var dataNow = inputData[i].value;
                    var label = '';

                    if (Array.isArray(docFormInfo[key])) {
                        dataPrev = docFormInfo[key][indexName[key]];
                        indexName[key]++;
                    } else {
                        dataPrev = docFormInfo[key];
                        label = $('[name="' + key + '"]').closest('td').prev().text();
                    }

                    if (dataPrev !== dataNow) {
                        var order = (indexName[key] === 0) ? '' : ' (' + indexName[key] + '번째)';
                        if (dataNow === undefined || dataNow === '') dataNow = '삭제';
                        modHistory += '○ ' + label + '(' + key + ')' + order + ' : ' + dataPrev + ' → ' + dataNow + '[LB]';
                    }
                }
            }
        }

        // 첨부파일 수정 이력
        //console.log(docFileInfoPrev);
        //console.log(docFileInfo);
        for (var key1 in docFileInfoPrev) {
            for (var key2 in docFileInfo) {
                if (key1 === key2) {
                    var numFile = (docFileInfoPrev[key1].length > docFileInfo[key2].length) ? docFileInfoPrev[key1].length : docFileInfo[key2].length;
                    for (var i = 0; i < numFile; i++) {
                        if (docFileInfoPrev[key1][i] === undefined) {
                            var label = $('[name="' + key1 + '[]"]').closest('td').prev().text();
                            var order = ' (' + (i + 1) + '번째)';
                            modHistory += '○ ' + label + '(' + key + ')' + order + ' : ' + '신규' + ' → ' + docFileInfo[key2][i].name + '[LB]';
                        } else if (docFileInfo[key2][i] === undefined) {
                            var label = $('[name="' + key1 + '[]"]').closest('td').prev().text();
                            var order = ' (' + (i + 1) + '번째)';
                            modHistory += '○ ' + label + '(' + key + ')' + order + ' : ' + docFileInfoPrev[key1][i].name + ' → ' + '삭제' + '[LB]';
                        } else if (docFileInfoPrev[key1][i].name !== docFileInfo[key2][i].name) {
                            var label = $('[name="' + key1 + '[]"]').closest('td').prev().text();
                            var order = ' (' + (i + 1) + '번째)';
                            modHistory += '○ ' + label + '(' + key + ')' + order + ' : ' + docFileInfoPrev[key1][i].name + ' → ' + docFileInfo[key2][i].name + '[LB]';
                        }
                    }
                }
            }
        }


        formData.append('mod_history', modHistory);
        //console.log(modHistory);

        $.ajax({
            url: settings.functionUrl,
            type: 'post',
            data: formData,
            processData: false,
            contentType: false,  // false로 설정해야 formData(file) 전송
            dataType: 'json',
            cache: false,
            beforeSend: function(xhr, settings) {
                displayLoadingPopup();
            },
            success: function(data, status, xhr) {
                getData = data;
                if (data.status === 'valid') {
                    settings.callbackSuccess(data);
                    // 메세지 출력
                    if (data.message !== '') {
                        alert(data.message);
                    }
                    // backend에서 전달된 url로 우선 처리 --> 그후 프론트 확인
                    if ((data.moveUrl === undefined || data.moveUrl === '') && data.moveUrl !== '') {
                        location.href = data.moveUrl;
                    } else if (settings.moveUrl !== '') {
                        location.href = settings.moveUrl;
                    } else {
                        //alert(location.href);
                        location.reload();
                    }
                } else if (data.status === 'invalid') {
                    invalidHandler(data.message, data.name, data.index);
                }
            },
            error: function(xhr, status, errorThrown) {
                // 서버 문서에서 오류(비정상적 종료)가 발생하는 경우(세부(출력)내용은 xhr.responseText에 포함)
                //console.log('error');
                alert('처리과정에서 오류가 발생하였습니다.');
                console.warn('오류 발생! [code : ' + xhr.status + '(' + status + '), message : ' + xhr.responseText + ', error : ' + errorThrown + ']');
            },
            complete: function(xhr, status) {  // success, error 인 경우 모두 발생
                settings.callbackComplete(getData);
                removeLoadingPopup();
            }
        });
    });   // click (ajax 전송)

    // 유효성 오류 처리
    function invalidHandler(message, name, index) {
        alert(message);
        index = (index === undefined) ? 0 : Number(index);        // 해당 index 없으면 0 (첫번째 요소)
        if ($('[name="' + name + '"]').length < 1) return false;  // 해당 요소 없으면 중단

        var $element = $('[name="' + name + '"]:eq(' + index + ')');   // 해당 인덱스 순서의 요소 (1개인 경우에는 첫번째)
        var formTagName = $element[0].tagName.toLowerCase();           // 태그명 식별
        var inputType = '';                                            // input 태그의 경우 type요소까지 식별

        if (formTagName === 'input') inputType = $element.attr('type');
        if (formTagName === 'input' && (inputType === 'checkbox' || inputType === 'radio' || inputType === 'file')) {
            $element.focus();                  // radio, checkbox 의 경우 포커스 안감 ?
        } else if (formTagName === 'select') {
            $element.focus();
        } else if (formTagName === 'textarea' && ($element.attr('class') !== undefined) && $element.attr('class').indexOf('editor') !== -1) {
            var index = $('textarea.editor').index($element);
            tinyMCE.editors[index].focus();  // editor 순서에 따라 선택
        } else {
            $element.select();
        }
    }

    // 로딩 이미지 표시
    function displayLoadingPopup() {
        $('body').append('<div id="loading-mask"><span><i class="fas fa-spinner fa-spin"></i></span></div>');
    }
    // 로딩 이미지 제거
    function removeLoadingPopup() {
        $('#loading-mask').remove();
    }
}



// 파일 첨부하기 모듈
$.fn.setFileUploader = function(options) {
    var settings = $.extend({
        maxNum: 0,                     // 0 : 제한없음, 1 : 기존파일 대체 방식으로 동작
        maxSize: 20,                   // 첨부파일(개별파일) 용량제한, 0 : 제한없음, 단위 : MB  --> 전체용량은 server에서 확인(100MB)
        extPermit: ['hwp', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'ai', 'psd', 'zip', 'egg', 'mov', 'avi', 'mp4', 'ogg', 'mp3', 'flv', 'swf', 'pdf', 'txt', 'tiff', 'pjp', 'pjpeg', 'jfif', 'tif', 'gif', 'svg', 'bmp', 'png', 'jpeg', 'svgz', 'jpg', 'webp', 'ico', 'sbm', 'dib'],
        getListUrl: '/admin/get-file-info',        // 서버에서 파일정보 가져오기 (php 경로)
        checkValidation: false
    }, options);

    // 여러 개인 경우 개별적으로 적용
    this.each(function() {
        var $selector = $(this);           // div.dino-file-list
        var $inputFile = null;             // <input type="file" name="file1[]" />
        var $inputGroup = null;            // <input type="hidden" name="file1_group" />
        var inputName = '';                // [] 없는 name 값 : file1
        var groupSeq = 0;                  // group value
        var mode = '';                     // view / edit(reg, mod)
        var fileInfo = [];                 // 첨부된 파일의 정보

        // 가용여부 검사 (input 관련 내용 검사)
        if ($selector.find('> input[type="file"]').length === 1) {     // 등록/수정인 경우
            $inputFile = $selector.find('> input[type="file"]');
            if ($inputFile.attr('name') === undefined || $inputFile.attr('name') === '') {
                alert('첨부파일 input 요소에 name 속성이 누락되었습니다.');
                return false;
            } else {
                inputName = $inputFile.attr('name').replace('[]', '');       // input file이 있고 name 속성이 입력된 경우 edit(reg/mod)로 간주
                mode = 'edit';
            }
        } else if ($selector.find('> input[type="file"]').length <= 0) {    // input file이 없으면 view로 간주
            mode = 'view';
        } else {
            alert('첨부파일 input 요소가 2개 이상 존재합니다.');
            return false;
        }
        // group 정보 저장용 input (등록/수정/보기 모두 필요)
        if ($selector.find('> input[type="hidden"]').length === 1) {
            $inputGroup = $selector.find('> input[type="hidden"]');
            groupSeq = $inputGroup.val() ? parseInt($inputGroup.val()) : 0;
        } else {
            alert('첨부파일 input group (type=hidden) 요소는 반드시 1개 있어야 합니다.');
            return false;
        }

        // 초기상태 설정(multiple, accept 속성)
        $selector.prepend('<ul></ul>');
        addNoFileList();
        if (mode === 'edit') {
            if (settings.maxNum === 1) {
                $inputFile.removeAttr('multiple');            // 1개인 경우 multiple 제외
            } else {
                $inputFile.attr({'multiple': 'multiple'});    // 다수 첨부 가능시 자동 multiple 지정
            }
        }


        // 기존 첨부파일 불러오기 (mode에 따라 첨부 / edit, view)
        if (groupSeq > 0) {
            var downAllLink = '';
            // 전송할 데이터 준비
            var formData = new FormData();
            formData.append('group_seq', groupSeq);

            // 기존 자료 저장
            docFileInfoPrev[inputName] = [];

            $.ajax({
                url: settings.getListUrl,
                type: 'post',            // post / get
                data: formData,     // 서버로 전송할 데이터
                processData: false,      // 보내는 데이터의 query string 변환 여부 (기본값은 true)
                contentType: false,      // false로 설정해야 formData(file) 전송
                dataType: 'json',        // 가져올 값의 형식(xml, json, script, html)
                cache: false,            // requested page 캐싱 여부 (기본값은 true)
                beforeSend: function(xhr, setting) {
                    //settings.callbackBefore();     // 전달할 정보 처리
                },
                success: function(data, status, xhr) {
                    for (var i = 0; i < data.length; i++) {
                        if (mode === 'edit') {
                            addFileListEdit(data[i]);
                            fileInfo[i] = data[i];
                            docFileInfoPrev[inputName][i] = data[i];
                        } else if (mode === 'view') {
                            addFileListView(data[i]);
                            downAllLink += '&file_seq[]=' + data[i].seq;
                        }
                    }
                },
                error: function(xhr, status, errorThrown) {
                    alert('처리과정[Ajax]에서 오류가 발생하였습니다.');
                    console.warn('오류 발생! [code : ' + xhr.status + '(' + status + '), message : ' + xhr.responseText + ', error : ' + errorThrown + ']');
                },
                complete: function(xhr, status) {  // success, error 인 경우 모두 발생
                    if (mode === 'view' && $selector.find('ul li').length > 1) {   // view에서 만
                        $selector.append('<p class="down-all"><a href="' + location.href + downAllLink + '" download="download"><i class="fas fa-arrow-down"></i>첨부파일 전체 다운로드 (ZIP)</a></p>');
                    }
                }
            });
            if (mode === 'edit') docFileInfo[inputName] = fileInfo;    // 참조 복사
        }


        // 신규 파일 첨부
        $selector.on('change', $inputFile, function(e) {
            addUploadingList();

            var files = e.target.files;        // 첨부된 파일 GET, IE 10 이상에서만 동작 (IE9 이하에서 오류)
            var numFile = files.length;        // 이번 첨부에 포함된 파일 수
            var errorMsg = '';                 // 오류 메시지
            var numPrev = $selector.find(' > ul > li.file').length;
            if (settings.maxNum === 1) numPrev = 0;    // 1개만 되는 경우에는 초기화 (덮어씀)

            // 유효성 검사(첨부파일 수)
            if (settings.checkValidation === true) {
                if (settings.maxNum > 1 && (numPrev + numFile) > settings.maxNum) {
                    // 1개만 되는 경우에는 그냥 덮어씀(검사 불필요 / 2개 선택도 불가)
                    errorMsg += '허용 가능한 첨부파일 수가 초과되었습니다. [허용가능 파일수 : ' + settings.maxNum + '개]\n';
                }
            }

            // 첨부파일 정보 정리 및 유효성 검사
            for (var i = 0; i < numFile; i++) {
                // 사전에 일부 정보 입력 (numPrev)
                fileInfo[numPrev + i] = {seq: 0, name: '', ext: '', type: '', size: 0, file: null};
                fileInfo[numPrev + i].name = files[i].name;
                fileInfo[numPrev + i].ext = files[i].name.slice(files[i].name.lastIndexOf(".") + 1).toLowerCase();
                fileInfo[numPrev + i].type = files[i].type;
                fileInfo[numPrev + i].size = files[i].size;
                fileInfo[numPrev + i].file = files[i];

                if (settings.checkValidation === true) {
                    // 유효성 검사(확장자, 개별 파일용량)
                    if (checkInArray(fileInfo[numPrev + i].ext, settings.extPermit) === -1) {
                        errorMsg += '첨부가 제한된 확장자입니다. [대상파일 : ' + fileInfo[i].name + ']\n';
                    }
                    if (settings.maxSize !== 0 && files[i].size > (settings.maxSize * 1024 * 1024)) {
                        errorMsg += '허용 가능한 파일용량(' + settings.maxSize + ' MB)이 초과되었습니다. [대상파일 : ' + fileInfo[i].name + ']\n';
                    }
                }
            } // 선택된 file 반복

            if (errorMsg !== '') {  // 오류 있는 경우
                return cancelUploading(errorMsg);
            } else {   // 오류 없는 경우
                for (var i = 0; i < numFile; i++) {
                    addFileListEdit({seq: 0, name: fileInfo[numPrev + i].name, size: fileInfo[numPrev + i].size});
                }
                docFileInfo[inputName] = fileInfo;
            }

            $selector.find('input[type="file"]:last').val('');
            //console.log(docFileInfo);
        });  // end of onchange event


        function addNoFileList() {
            var htmlNoFile = '<li class="no-file"><i class="fas fa-exclamation-triangle"></i>첨부된 파일이 없습니다.</li>';
            $selector.find('> ul').html(htmlNoFile); // 기존 리스트 모두 제거후 입력
        }

        function addUploadingList() {
            var htmlUploading = '<li class="uploading"><i class="fas fa-spinner fa-spin"></i>Uploading...</li>';
            $selector.find('> ul li.no-file').remove();
            $selector.find('> ul').append(htmlUploading);
        }

        function addFileListView(data) {  // data.seq, data.name, data.size(MB);
            var seq = data.seq;
            var name = data.name;
            var size = data.size;
            var ext = name.substring(name.lastIndexOf('.') + 1, name.length).toLowerCase();
            var sizeMB = (size / 1024 / 1024).toFixed(2);
            var url = encodeURI(location.href);   // 안하면 &reg 등 특수기호로 나옴
            var htmlFileListEdit = '\
                <li class="file">\
                    <a href="' + url + '&file_seq[]=' + data.seq + '" class="filename" title="' + name + '" download="download">\
                        <img alt="" src="/admin/img/common/' + findIconFile(ext) + '" />\
                        <em>' + name + '</em>\
                        <i class="fas fa-arrow-down"></i>\
                        <span class="size">' + sizeMB + ' MB</span>\
                    </a>\
                </li>\
            ';
            // 크롬에서 다운로드용 a link에 donwload 속성을 기록해야 warning 안뜸
            $selector.find('> ul li.no-file').remove();
            $selector.find('> ul li.uploading').remove();
            if (settings.maxNum === 1) {    // 1개만 첨부 가능한 경우 기존 파일 대체
                $selector.find('> ul').empty();
                numPrev = 0;
            }
            $selector.find('> ul').append(htmlFileListEdit);
        }

        function addFileListEdit(data) {  // data.seq, data.name, data.size(MB);
            var seq = data.seq;
            var name = data.name;
            var size = data.size;
            var ext = name.substring(name.lastIndexOf('.') + 1, name.length).toLowerCase();
            var sizeMB = (size / 1024 / 1024).toFixed(2);
            var htmlFileListEdit = '\
                <li class="file">\
                    <span class="filename" title="' + name + '">\
                        <img alt="" src="/admin/img/common/' + findIconFile(ext) + '" />\
                        <em>' + name + '</em>\
                    </span>\
                    <span class="size">' + sizeMB + ' MB</span>\
                    <p class="function">\
                        <a href="#" class="prev" title="이전으로 이동"><i class="fas fa-arrow-up"><span>이전으로 이동</span></i></a>\
                        <a href="#" class="next" title="다음으로 이동"><i class="fas fa-arrow-down"><span>다음으로 이동</span></i></a>\
                        <a href="#" class="del" title="삭제"><i class="fas fa-times"><span>삭제</span></i></a>\
                    </p>\
                    <input type="hidden" name="' + inputName + '_seq[]" value="' + seq + '" />\
                </li>\
            ';
            $selector.find('> ul li.no-file').remove();
            $selector.find('> ul li.uploading').remove();
            if (settings.maxNum === 1) {    // 1개만 첨부 가능한 경우 기존 파일 대체
                $selector.find('> ul').empty();
                numPrev = 0;
            }
            $selector.find('> ul').append(htmlFileListEdit);
        }

        // 오류 메세지 출력 후 초기화
        function cancelUploading(msg) {
            alert(msg);
            $selector.find('> ul li.uploading').remove();
            if ($selector.find('li.file').length === 0) {
                addNoFileList();
            }
            $selector.find('input[name="' + inputName + '[]"]:last').val('');
            return false;
        }

        // 리스트 삭제
        $selector.on('click', 'li.file a.del', function() {
            if (confirm('첨부된 파일을 리스트에서 삭제하시겠습니까? 변경내용 저장 시 실제로 삭제됩니다.')) {
                var index = $selector.find('> ul > li').index($(this).closest('li'));
                $(this).closest('li').remove();
                if ($selector.find('li.file').length === 0) {
                    addNoFileList();
                }
                fileInfo.splice(index, 1);
                docFileInfo[inputName] = fileInfo;
            }
        });

        // 리스트 위로 이동
        $selector.on('click', 'li.file a.prev', function() {
            var index = $selector.find('> ul > li').index($(this).closest('li'));
            var clonedList = $(this).closest('li').clone();
            if (index === 0) {
                alert('가장 앞에 위치한 리스트입니다.');
                return false;
            }
            $(this).closest('li').prev().before(clonedList);
            $(this).closest('li').remove();
            fileInfo.splice((index - 1), 2, fileInfo[index], fileInfo[index - 1]);
            docFileInfo[inputName] = fileInfo;
        });

        // 리스트 아래로 이동
        $selector.on('click', 'li.file a.next', function() {
            var index = $selector.find('> ul > li').index($(this).closest('li'));
            var clonedList = $(this).closest('li').clone();
            if (index === $selector.find('> ul > li').length - 1) {
                alert('가장 뒤에 위치한 리스트입니다.');
                return false;
            }
            $(this).closest('li').next().after(clonedList);
            $(this).closest('li').remove();
            fileInfo.splice(index, 2, fileInfo[index + 1], fileInfo[index]);
            docFileInfo[inputName] = fileInfo;
        });

        // 첨부파일 다운로드
        /*
        $selector.on('click', 'li.file a.filename', function() {
            var seq = $(this).attr('data-seq');
            var downloadUrl = location.href + '&file_seq[]=' + seq;
            location.href = downloadUrl;
        });
        */
    });   // end of each event
}



// 이미지 파일 첨부
$.fn.setImageUploader = function(options) {
    var settings = $.extend({
        maxNum: 0,                    // 0 : 제한없음, 1 : 기존파일 대체 방식으로 동작
        maxSize: 10,                  // 첨부파일(개별파일) 용량제한, 0 : 제한없음, 단위 : MB  --> 전체용량은 server에서 확인
        maxWidth: 1920,               // max를 넘는 경우 체크 혹은 자동 resize
        maxHeight: 1080,              // max를 넘은 경우 체크 혹은 자동 resize
        minWidth: 0,
        minHeight: 0,
        permitExt: ['tiff', 'pjp', 'pjpeg', 'jfif', 'tif', 'gif', 'svg', 'bmp', 'png', 'jpeg', 'svgz', 'jpg', 'webp', 'ico', 'sbm', 'dib'],
        getListUrl: '/admin/get-file-info',         // 서버에서 파일정보 가져오기 (php 경로)
        setAccept : true,             // accept속성 지정 여부 (accept="image/*")
        autoResize: true,             // true이면 자동 resize  (설정된 max width/height를 넘어가면 max 값에 맞게 재설정 / max가 0이면 제외)
        autoRotate: true,             // 모바일 가로/세로 이미지가 자동으로 회전되는 것 조정 (촬영형태 그대로 반영)
        rotate: true,                 // rotate option 활성화 여부
        checkValidation: false
    }, options);

    this.each(function() {
        var $selector = $(this);           // div.dino-image-list
        var $inputFile = null;             // <input type="file" name="file1[]" />
        var $inputGroup = null;            // <input type="hidden" name="file1_group" />
        var inputName = '';                // [] 없는 name 값 : file1
        var groupSeq = 0;                  // group value
        var mode = '';                     // view / edit(reg, mod)
        var fileInfo = [];                 // 첨부된 파일의 정보

        // 가용여부 검사 (input 관련 내용 검사)
        if ($selector.find('> input[type="file"]').length === 1) {     // 등록/수정인 경우
            $inputFile = $selector.find('> input[type="file"]');
            if ($inputFile.attr('name') === undefined || $inputFile.attr('name') === '') {
                alert('첨부파일 input 요소에 name 속성이 누락되었습니다.');
                return false;
            } else {
                inputName = $inputFile.attr('name').replace('[]', '');       // input file이 있고 name 속성이 입력된 경우 edit(reg/mod)로 간주
                mode = 'edit';
            }
        } else if ($selector.find('> input[type="file"]').length <= 0) {    // input file이 없으면 view로 간주
            mode = 'view';
        } else {
            alert('첨부파일 input 요소가 2개 이상 존재합니다.');
            return false;
        }
        // group 정보 저장용 input (등록/수정/보기 모두 필요)
        if ($selector.find('> input[type="hidden"]').length === 1) {
            $inputGroup = $selector.find('> input[type="hidden"]');
            groupSeq = $inputGroup.val() ? parseInt($inputGroup.val()) : 0;
        } else {
            alert('첨부파일 input group (type=hidden) 요소는 반드시 1개 있어야 합니다.');
            return false;
        }

        // 초기상태 설정(multiple, accept 속성)
        $selector.prepend('<ul></ul>');
        addNoImageList();
        if (mode === 'edit') {
            if (settings.maxNum === 1) {
                $inputFile.removeAttr('multiple');            // 1개인 경우 multiple 제외
            } else {
                $inputFile.attr({'multiple': 'multiple'});    // 다수 첨부 가능시 자동 multiple 지정
            }
            if (settings.setAccept === true) {
                $inputFile.attr({'accept': 'image/*'});
            } else {
                $inputFile.removeAttr('accept');
            }
        }


        // 기존 첨부내용이 있는 경우 (view / edit)
        if (groupSeq > 0) {

            // 전송할 데이터 준비
            var formData = new FormData();
            formData.append('group_seq', groupSeq);

            // 기존 자료 저장
            docFileInfoPrev[inputName] = [];

            $.ajax({
                url: settings.getListUrl,
                type: 'post',            // post / get
                data: formData,     // 서버로 전송할 데이터
                processData: false,      // 보내는 데이터의 query string 변환 여부 (기본값은 true)
                contentType: false,      // false로 설정해야 formData(file) 전송
                dataType: 'json',        // 가져올 값의 형식(xml, json, script, html)
                cache: false,            // requested page 캐싱 여부 (기본값은 true)
                beforeSend: function(xhr, setting) {
                    //settings.callbackBefore();     // 전달할 정보 처리
                },
                success: function(data, status, xhr) {
                    for (var i = 0; i < data.length; i++) {
                        if (mode === 'edit') {
                            addImageListEdit(data[i]);
                            fileInfo[i] = data[i];
                            docFileInfoPrev[inputName][i] = data[i];
                        } else if (mode === 'view') {
                            addImageListView(data[i]);
                        }
                    }
                },
                error: function(xhr, status, errorThrown) {
                    alert('처리과정[Ajax]에서 오류가 발생하였습니다.');
                    console.warn('오류 발생! [code : ' + xhr.status + '(' + status + '), message : ' + xhr.responseText + ', error : ' + errorThrown + ']');
                },
                complete: function(xhr, status) {  // success, error 인 경우 모두 발생

                }
            });
            if (mode === 'edit') docFileInfo[inputName] = fileInfo;    // 참조 복사
        }


        // 신규 파일 첨부
        $selector.on('change', $inputFile, function(e) {
            addUploadingList();

            var files = e.target.files;           // 첨부된 파일 GET, IE 10 이상에서만 동작 (IE9 이하에서 오류)
            var numFile = files.length;           // 이번 첨부에 포함된 파일 수 (file read, image load 에 활용)
            var errorMsg = '';                    // 오류 메시지
            var indexFileRead = 0;                // file load 용 index
            var indexImageLoad = 0;               // image load 용 index
            var numPrev = $selector.find(' > ul > li.image').length;
            if (settings.maxNum === 1) numPrev = 0;    // 1개만 되는 경우에는 초기화 (덮어씀)

            // 유효성 검사(첨부파일 수)
            if (settings.checkValidation === true) {
                if (settings.maxNum > 1 && (numPrev + numFile) > settings.maxNum) {
                    // 1개만 되는 경우에는 그냥 덮어씀(검사 불필요 / 2개 선택도 불가)
                    errorMsg += '허용 가능한 첨부파일 수가 초과되었습니다. [허용가능 파일수 : ' + settings.maxNum + '개]\n';
                    return cancelUploading(errorMsg);
                }
            }


            // 첨부파일 정보 정리 및 유효성 검사
            for (var i = 0; i < numFile; i++) {
                fileInfo[numPrev + i] = {seq: 0, name: '', ext: '', type: '', size: 0, src: '', width: 0, height: 0, isResized: 'N', orientation: 0, rotate: 0, file: null};
                fileInfo[numPrev + i].name = files[i].name;
                fileInfo[numPrev + i].ext = files[i].name.slice(files[i].name.lastIndexOf(".") + 1).toLowerCase();
                fileInfo[numPrev + i].type = files[i].type;
                fileInfo[numPrev + i].size = files[i].size;
                fileInfo[numPrev + i].file = files[i];

                // orientation 정보 세팅 (image autoRotate보다는 앞에서 실행 종료)
                getOrientation(fileInfo[numPrev + i].file, fileInfo[numPrev + i]);

                // 유효성 검사(확장자, 개별 파일용량)
                if (settings.checkValidation === true) {
                    if (checkInArray(fileInfo[numPrev + i].ext, settings.permitExt) === -1) {
                        errorMsg += '첨부가 제한된 확장자입니다. [대상파일 : ' + fileInfo[numPrev + i].name + ']\n';
                        return cancelUploading(errorMsg);
                    }
                    if (settings.maxSize !== 0 && fileInfo[numPrev + i].size > (settings.maxSize * 1024 * 1024)) {
                        errorMsg += '허용 가능한 파일용량(' + settings.maxSize + ' MB)이 초과되었습니다. [대상파일 : ' + fileInfo[numPrev + i].name + ']\n';
                        return cancelUploading(errorMsg);
                    }
                }

                // 이미지 파일 로드
                var reader = new FileReader();
                reader.readAsDataURL(fileInfo[numPrev + i].file);
                reader.onload = function(e) {
                    fileInfo[numPrev + indexFileRead].src = this.result;      // 다른 시점에 실행되므로 index 별도로 찾아줘야 함 (이 코드 실행시점의 i는 다른 값임 / indexFileRead)


                    //fileInfo[numPrev + indexFileRead].orientation = getImageOrientation(this);
                    //console.log(fileInfo[numPrev + indexFileRead].orientation);

                    // 이미지 생성
                    var image = new Image();
                    image.src = fileInfo[numPrev + indexFileRead].src;        // [이미지 경로] 다른 시점에 실행되므로 index 별도로 찾아줘야 함 (이 코드 실행시점의 i는 다른 값임 / indexImageLoad)
                    image.onload = function() {                               // 첨부된 파일이 이미지인 경우에만 실행됨
                        fileInfo[numPrev + indexImageLoad].width = this.width;      // [이미지 폭]
                        fileInfo[numPrev + indexImageLoad].height = this.height;    // [이미지 높이]

                        if (settings.checkValidation === true) {
                            // 이미지 크기 검사
                            if (settings.minWidth !== 0 && this.width < settings.minWidth) {
                                errorMsg += '첨부 이미지는 폭이 최소 ' + settings.minWidth + 'px 이상 이어야 합니다. [대상파일 : ' + fileInfo[numPrev + indexImageLoad].name + ']\n';
                            }
                            if ((settings.autoResize !== true && settings.maxWidth !== 0) && this.width > settings.maxWidth) {    // autoResize가 true 이면 자동 조정 (검사 불필요)
                                errorMsg += '첨부 이미지는 폭이 최대 ' + settings.maxWidth + 'px 이하 이어야 합니다. [대상파일 : ' + fileInfo[numPrev + indexImageLoad].name + ']\n';
                            }
                            if (settings.minHeight !== 0 && this.height < settings.minHeight) {
                                errorMsg += '첨부 이미지는 높이가 최소 ' + settings.minHeight + 'px 이상 이어야 합니다. [대상파일 : ' + fileInfo[numPrev + indexImageLoad].name + ']\n';
                            }
                            if ((settings.autoResize !== true && settings.maxHeight !== 0) && this.height > settings.maxHeight) {  // autoResize가 true 이면 자동 조정 (검사 불필요)
                                errorMsg += '첨부 이미지는 높이가 최대 ' + settings.maxHeight + 'px 이하 이어야 합니다. [대상파일 : ' + fileInfo[numPrev + indexImageLoad].name + ']\n';
                            }
                        }

                        // 허용된 이미지 크기를 초과하는 경우 이미지 조정
                        if (settings.autoResize === true) {
                            if ((settings.maxWidth > 0 && this.width > settings.maxWidth) || (settings.maxHeight > 0 && this.height > settings.maxHeight)) {
                                var fileInfoResized = resizeImage(this, fileInfo[numPrev + indexImageLoad].type, settings.maxWidth, settings.maxHeight);
                                for (var key in fileInfoResized) {
                                    fileInfo[numPrev + indexImageLoad][key] = fileInfoResized[key];    // width, height, file, size, src, isResized 수정 반영
                                }
                            }
                        }

                        // 모바일 orientation 고려하여 자동 회전
                        if (settings.autoRotate === true) {
                            var fileInfoRotated = {};
                            var fileInfoTemp = fileInfo[numPrev + indexImageLoad];
                            var varUA = navigator.userAgent.toLowerCase(); //userAgent 값 얻기

                            if (fileInfoTemp.orientation === 6) {     // 세로로 찍은 경우
                                // 아이폰은 자동으로 회전하여 세로가 길게 나옴 (가로가 긴 경우만 적용)
                                // 안드로이드의 경우 자동으로 +90도로 회전 / 반대로 90도 돌려줘야 세로로 표시
                                if (fileInfoTemp.width > fileInfoTemp.height) {
                                    fileInfoRotated = rotateImage(this, fileInfoTemp.type, fileInfoTemp.width, fileInfoTemp.height, 'right');
                                }
                            } else if (fileInfoTemp.orientation === 8) {     // 세로로 뒤집어 찍은 경우
                                if (fileInfoTemp.width > fileInfoTemp.height) {
                                    fileInfoRotated = rotateImage(this, fileInfoTemp.type, fileInfoTemp.width, fileInfoTemp.height, 'left');
                                }
                            } else if (fileInfoTemp.orientation === 3) {     // 가로로 뒤집어 찍은 경우
                                if (varUA.match('android') !== null) {        // android 인 경우 (180도 회전)
                                    fileInfoRotated = rotateImage(this, fileInfoTemp.type, fileInfoTemp.width, fileInfoTemp.height, 'reverse');
                                }
                            }

                            // 파일 정보 재설정
                            for (var key in fileInfoRotated) {
                                if (key === 'rotate') {
                                    fileInfo[numPrev + indexImageLoad][key] += fileInfoRotated[key];   // 숫자 누적 (+- 90);
                                } else {
                                    fileInfo[numPrev + indexImageLoad][key] = fileInfoRotated[key];    // width, height, file, size, src, rotate 수정 반영
                                }
                            }
                        }

                        indexImageLoad++;

                        // 최종 이미지가 로드된 후
                        if (indexImageLoad === numFile) {
                            if (errorMsg !== '') {  // 오류 있는 경우
                                return cancelUploading(errorMsg);
                            } else {   // 오류 없는 경우
                                for (var j = 0; j < numFile; j++) {
                                    addImageListEdit({
                                        seq: 0,
                                        name: fileInfo[numPrev + j].name,
                                        size: fileInfo[numPrev + j].size,
                                        src: fileInfo[numPrev + j].src,
                                        width: fileInfo[numPrev + j].width,
                                        height: fileInfo[numPrev + j].height
                                    });
                                }
                                docFileInfo[inputName] = fileInfo;

                                // console.log(docFileInfo);
                            }
                        }
                    }  // image onload

                    indexFileRead++;
                }   // file reader

            } // 선택된 file 반복

            $selector.find('input[type="file"]:last').val('');
        });  // end of onchange event



        function addNoImageList() {
            var htmlNoImage = '\
                <li class="no-image">\
                    <span class="image"><i class="fas fa-exclamation-triangle"></i></span>\
                    <em class="filename"><i class="fas fa-exclamation-triangle"></i>첨부된 파일이 없습니다.</em>\
                </li>\
            ';
            $selector.find('> ul').html(htmlNoImage); // 기존 리스트 모두 제거후 입력
        }

        function addUploadingList() {
            var htmlUploading = '\
                <li class="uploading">\
                    <span class="image"><i class="fas fa-spinner fa-spin"></i></span>\
                    <em class="filename"><i class="fas fa-spinner fa-spin"></i>Uploading...</em>\
                </li>\
            ';
            $selector.find('> ul li.no-image').remove();
            $selector.find('> ul').append(htmlUploading);
        }

        function cancelUploading(msg) {
            alert(msg);
            $selector.find('> ul li.uploading').remove();
            if ($selector.find('li.image').length === 0) {
                addNoImageList();
            }
            $selector.find('input[name="' + inputName + '[]"]:last').val('');
            return false;
        }

        function addImageListView(data) {  // data.seq, data.src, data.size(MB)), index가 있는 경우 해당 index의 이미지를 대체
            var ext = data.name.substring(data.name.lastIndexOf('.') + 1, data.name.length).toLowerCase();
            var sizeMB = (data.size / 1024 / 1024).toFixed(2);
            var htmlRoate = (settings.rotate === true) ? '<a href="#" class="rotate" title="90도 회전"><i class="fas fa-sync"><span>90도 회전</span></i></a>' : '';
            var htmlImageListEdit = '\
                <li class="image">\
                    <span class="image">\
                        <img alt="" src="' + data.src + '">\
                        <span class="dimension">' + data.width + 'px X ' + data.height + 'px</span>\
                    </span>\
                    <em class="filename" title="' + data.name + '"><img alt="" src="/admin/img/common/' + findIconFile(ext) + '" />' + data.name + '</em>\
                    <span class="size">' + sizeMB + ' MB</span>\
                    <p class="function">\
                        <a href="#" class="enlarge" title="원본보기"><i class="fas fa-search-plus"><span>원본보기</span></i></a>\
                    </p>\
                </li>\
            ';
            $selector.find('> ul li.no-image').remove();
            $selector.find('> ul li.uploading').remove();
            if (settings.maxNum === 1) {
                $selector.find('> ul').empty();
                numNewList = 0;
            }
            $selector.find('> ul').append(htmlImageListEdit);
        }

        function addImageListEdit(data, index) {  // data.seq, data.src, data.size(MB)), index가 있는 경우 해당 index의 이미지를 대체
            var ext = data.name.substring(data.name.lastIndexOf('.') + 1, data.name.length).toLowerCase();
            var sizeMB = (data.size / 1024 / 1024).toFixed(2);
            var htmlRoate = (settings.rotate === true) ? '<a href="#" class="rotate" title="90도 회전"><i class="fas fa-sync"><span>90도 회전</span></i></a>' : '';
            var htmlImageListEdit = '\
                <li class="image">\
                    <span class="image">\
                        <img alt="" src="' + data.src + '">\
                        <span class="dimension">' + data.width + 'px X ' + data.height + 'px</span>\
                    </span>\
                    <em class="filename" title="' + data.name + '"><img alt="" src="/admin/img/common/' + findIconFile(ext) + '" />' + data.name + '</em>\
                    <span class="size">' + sizeMB + ' MB</span>\
                    <p class="function">\
                        <a href="#" class="prev" title="이전으로 이동"><i class="fas fa-arrow-left"><span>이전으로 이동</span></i></a>\
                        <a href="#" class="next" title="다음으로 이동"><i class="fas fa-arrow-right"><span>다음으로 이동</span></i></a>'
                        + htmlRoate +
                        '<a href="#" class="del" title="삭제"><i class="fas fa-times"><span>삭제</span></i></a>\
                    </p>\
                    <input type="hidden" name="' + inputName + '_seq[]" value="' + data.seq + '" />\
                </li>\
            ';
            $selector.find('> ul li.no-image').remove();
            $selector.find('> ul li.uploading').remove();

            if (settings.maxNum === 1) {   // index 무시
                $selector.find('> ul').empty();
                numNewList = 0;
                $selector.find('> ul').append(htmlImageListEdit);
            } else {
                if (index !== undefined && index >= 0) {   // index에 해당하는 이미지와 교체
                    $selector.find('> ul li.image:eq(' + index + ')').after(htmlImageListEdit);
                    $selector.find('> ul li.image:eq(' + index + ')').remove();
                } else {
                    $selector.find('> ul').append(htmlImageListEdit);
                }
            }
        }

        // 첨부 이미지 삭제
        $selector.on('click', 'li.image a.del', function() {
            if (confirm('첨부된 이미지를 리스트에서 삭제하시겠습니까? 변경내용 저장 시 실제로 삭제됩니다.')) {
                var index = $selector.find('> ul > li').index($(this).closest('li'));
                $(this).closest('li').remove();
                if ($selector.find('li.image').length === 0) {
                    addNoImageList();
                }
                fileInfo.splice(index, 1);
                docFileInfo[inputName] = fileInfo;
            }
        });

        $selector.on('click', 'li.image a.prev', function() {
            var index = $selector.find('> ul > li').index($(this).closest('li'));
            var clonedList = $(this).closest('li').clone();
            if (index === 0) {
                alert('가장 앞에 위치한 리스트입니다.');
                return false;
            }
            $(this).closest('li').prev().before(clonedList);
            $(this).closest('li').remove();
            fileInfo.splice((index - 1), 2, fileInfo[index], fileInfo[index - 1]);
            docFileInfo[inputName] = fileInfo;
        });

        $selector.on('click', 'li.image a.next', function() {
            var index = $selector.find('> ul > li').index($(this).closest('li'));
            var clonedList = $(this).closest('li').clone();
            if (index === $selector.find('> ul > li').length - 1) {
                alert('가장 뒤에 위치한 리스트입니다.');
                return false;
            }
            $(this).closest('li').next().after(clonedList);
            $(this).closest('li').remove();
            fileInfo.splice(index, 2, fileInfo[index + 1], fileInfo[index]);
            docFileInfo[inputName] = fileInfo;
        });

        // 90도 회전 (기존 첨부 이미지 회전하면? seq=0 --> 재업로드)
        $selector.on('click', 'li.image a.rotate', function() {
            var $target = $(this).closest('li.image').find('span.image img');
            var index = $selector.find('li.image').index($(this).closest('li.image'));
            var image = new Image();
            image.src = fileInfo[index].src;
            image.onload = function() {
                var fileInfoRotated = rotateImage(this, fileInfo[index].type, fileInfo[index].width, fileInfo[index].height, 'right');
                for (var key in fileInfoRotated) {
                    if (key === 'rotate') {
                        fileInfo[index][key] = (fileInfo[index][key] === undefined) ? fileInfoRotated[key] : fileInfo[index][key] + fileInfoRotated[key];   // 숫자 누적 (+- 90);
                    } else {
                        fileInfo[index][key] = fileInfoRotated[key];    // width, height, file, size, src, rotate 수정 반영
                    }
                }
                fileInfo[index].seq = 0;

                addImageListEdit({
                    seq: 0,            // 0 --> 새롭게 insert 가능
                    name: fileInfo[index].name,
                    size: fileInfo[index].size,
                    src: fileInfo[index].src,
                    width: fileInfo[index].width,
                    height: fileInfo[index].height
                }, index);
            }
        });

        $selector.on('click', 'li.image a.enlarge', function() {
            var imgSrc = $(this).closest('li').find('span.image img').attr('src');
            $('body').append('\
                <div id="popup-image">\
                    <a href="#" class="close"><i class="fas fa-times"><span>팝업창 닫기</span></i></a>\
                    <p><img alt="" src="' + imgSrc + '" /></p>\
                </div>');
            $('#popup-image').one('click', 'a.close', function() {
                $('#popup-image').remove();
            });
        });

    });     // end of each
}


// 이미지 리사이즈, image : 이미지 객체(image load), type, max width/height가 0이미 조정하지 않음
function resizeImage(image, type, maxWidth, maxHeight) {
    var newWidth = 0;
    var newHeight = 0;
    var fileInfo = {};

    if (maxWidth > 0 && image.width > maxWidth) {
        newWidth = maxWidth;
        newHeight = parseInt((image.height / image.width) * newWidth);
        if (maxHeight > 0 && newHeight > maxHeight) {
            newHeight = maxHeight;
            newWidth = parseInt((this.width / this.height) * newHeight);
        }
    }
    if (maxHeight > 0 && image.height > maxHeight) {
        newHeight = maxHeight;
        newWidth = parseInt((image.width / image.height) * newHeight);
        if (maxWidth > 0 && newWidth > maxWidth) {
            newWidth = maxWidth;
            newHeight = parseInt((image.height / image.width) * newWidth);
        }
    }

    // 이미지 크기 조정
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');
    canvas.setAttribute('width', newWidth);
    canvas.setAttribute('height', newHeight);
    ctx.drawImage(image, 0, 0, newWidth, newHeight);

    var dataUrl = canvas.toDataURL(type);
    var blobBin = atob(dataUrl.split(',')[1]);
    var array = [];
    for(var i = 0; i < blobBin.length; i++) {
        array.push(blobBin.charCodeAt(i));
    }

    // 변경된 이미지로 정보 변경
    fileInfo.width = newWidth;
    fileInfo.height = newHeight;
    fileInfo.file = new Blob([new Uint8Array(array)], {type: type});
    fileInfo.src = dataUrl;
    fileInfo.size = fileInfo.file.size;
    fileInfo.isResized = 'Y';

    return fileInfo;
}

// 이미지 회전, image : 이미지 객체(image load), type, max width/height가 0이미 조정하지 않음
function rotateImage(image, type, width, height, direction) {
    var newWidth = height;
    var newHeight = width;
    var fileInfo = {};

    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');
    ctx.translate(0, 0);
    if (direction === 'right') {    // 시계방향 90도 회전
        canvas.setAttribute('width', newWidth);
        canvas.setAttribute('height', newHeight);
        ctx.rotate(90 * Math.PI / 180);
        ctx.drawImage(image, 0, -newWidth, newHeight, newWidth);
    } else if (direction === 'left') {     // 반시계방향 90도 회전
        canvas.setAttribute('width', newWidth);
        canvas.setAttribute('height', newHeight);
        ctx.rotate(-90 * Math.PI / 180);
        ctx.drawImage(image, -newHeight, 0, newHeight, newWidth);
    } else if (direction === 'reverse') {   // 뒤집기
        canvas.setAttribute('width', width);
        canvas.setAttribute('height', height);
        ctx.rotate(180 * Math.PI / 180);
        ctx.drawImage(image, -width, -height, width, height);
    }

    var dataUrl = canvas.toDataURL(type);
    var blobBin = atob(dataUrl.split(',')[1]);
    var array = [];
    for(var i = 0; i < blobBin.length; i++) {
        array.push(blobBin.charCodeAt(i));
    }

    // 변경된 이미지로 정보 변경
    fileInfo.width = newWidth;
    fileInfo.height = newHeight;
    fileInfo.file = new Blob([new Uint8Array(array)], {type: type});
    fileInfo.size = fileInfo.file.size;
    fileInfo.src = dataUrl;
    fileInfo.rotate = (direction === 'right') ? 90 : -90;

    return fileInfo;
}

// 이미지 orientation 정보 가져오기, obj : orientation 정보를 저장할 객체 요소
function getOrientation(file, obj) {
    var orientation = 0;
    var reader = new FileReader();
    reader.readAsArrayBuffer(file.slice(0, 64 * 1024));
    reader.onload = function(event) {
        var view = new DataView(event.target.result);
        if (view.getUint16(0, false) != 0xFFD8) {
            obj.orientation = -2;
            return -2;
        }

        var length = view.byteLength;
        var offset = 2;
        while (offset < length) {
            var marker = view.getUint16(offset, false);
            offset += 2;

            if (marker == 0xFFE1) {
                if (view.getUint32(offset += 2, false) != 0x45786966) {
                    obj.orientation = -1;
                    return -1;
                }
                var little = view.getUint16(offset += 6, false) == 0x4949;
                offset += view.getUint32(offset + 4, little);
                var tags = view.getUint16(offset, little);
                offset += 2;

                for (var i = 0; i < tags; i++) {
                    if (view.getUint16(offset + (i * 12), little) == 0x0112) {
                        obj.orientation = view.getUint16(offset + (i * 12) + 8, little);
                        return parseInt(view.getUint16(offset + (i * 12) + 8, little));
                    }
                }
            } else if ((marker & 0xFF00) != 0xFF00) {
                break;
            } else {
                offset += view.getUint16(offset, false);
            }
        }
        obj.orientation = -1;
        return -1;
    };
};

// 배열 존재여부 식별
function checkInArray(el, arr) {
    var result = -1;
    for (var i = 0; i < arr.length; i++) {
        if (el === arr[i]) return i;
    }
    return result;
}

// 공통 아이콘 식별 함수
function findIconFile(ext) {
    var iconFileName = '';
    switch(ext) {
        case 'tiff':
        case 'pjp':
        case 'pjpeg':
        case 'jfif':
        case 'tif':
        case 'gif':
        case 'svg':
        case 'bmp':
        case 'png':
        case 'jpeg':
        case 'svgz':
        case 'jpg':
        case 'webp':
        case 'ico':
        case 'sbm':
        case 'dib': iconFileName = 'icon_img.png'; break;
        case 'ppt':
        case 'pptx': iconFileName = 'icon_ppt.png'; break;
        case 'xls':
        case 'xlsx': iconFileName = 'icon_excel.png'; break;
        case 'doc':
        case 'docx': iconFileName = 'icon_doc.png'; break;
        case 'hwp': iconFileName = 'icon_hwp.png'; break;
        case 'zip':
        case 'egg': iconFileName = 'icon_zip.png'; break;
        case 'mp4':
        case 'avi':
        case 'mov':
        case 'ogg': iconFileName = 'icon_mov.png'; break;
        case 'mp3': iconFileName = 'icon_mp3.png'; break;
        case 'ai': iconFileName = 'icon_ai.png'; break;
        case 'psd': iconFileName = 'icon_psd.png'; break;
        case 'pdf': iconFileName = 'icon_pdf.png'; break;
        case 'txt': iconFileName = 'icon_txt.png'; break;
        default: iconFileName = 'icon_file.png';
    }
    return iconFileName;    // 아이콘 파일명 반환
}




function setColumSelect($pageId) {
    // 기존 정보 세팅(select box만)
    $('section.select-column select').each(function() {
        var $selectForm = $(this);
        var data = $selectForm.attr('data-select');
        $selectForm.find('option').each(function() {
            var optionValue = $(this).attr('value');
            if (data === optionValue) {
                $(this).prop({'selected': true});
            } else {
                $(this).prop({'selected': false});
            }
        });
    });

    // column 열기/닫기
    $('ul.search-list + div.button-box a.btn.column').on('click', function() {
        if ($(this).attr('class').indexOf('reverse') !== -1) {  // reverse 이 있으면 (닫음)
            $('section.select-column').css({'height': 0, 'margin-bottom': '-20px'});
            $(this).removeClass('reverse');
        } else {
            var height = $('section.select-column > section:eq(0)').outerHeight();
            $('section.select-column').css({'height': height + 'px', 'margin-bottom': '20px'});
            $(this).addClass('reverse');
        }
    });

    // 정렬기능
    $('section.select-column ul.input-list li a.sort').on('click', function() {
        var className = $(this).attr('class');
        var numPrev = $(this).closest('ul').find('a.sort.asc, a.sort.desc').length;
        if (className.indexOf('no') !== -1) {
            $(this).removeClass('no').addClass('asc');
            $(this).parent().find('input[name="select_column_sort[]"]').val('asc');
            $(this).attr({'title': '오름차순'});
            // 번호 추가
            $(this).closest('li').find('[name="select_column_order[]"]').val(numPrev + 1);
            $(this).closest('li').find('a.sort span.order').text(numPrev + 1);
        } else if (className.indexOf('asc') !== -1) {
            $(this).removeClass('asc').addClass('desc');
            $(this).parent().find('input[name="select_column_sort[]"]').val('desc');
            $(this).attr({'title': '내림차순'});
            // 번호 변화 없음
        } else if (className.indexOf('desc') !== -1) {
            var orderDel = Number($(this).closest('li').find('[name="select_column_order[]"]').val());
            $(this).removeClass('desc').addClass('no');
            $(this).parent().find('[name="select_column_sort[]"]').val('no');
            $(this).attr({'title': '미적용'});
            // 번호 삭제
            $(this).closest('li').find('[name="select_column_order[]"]').val(0);
            $(this).closest('li').find('a.sort span.order').text(0);
            // 전체적으로 번호 조정 (기존 선택 내용 모두 1씩 빼야 함);
            $(this).closest('ul').find('> li').each(function() {
                if ($(this).find('a.sort').length === 1) {   // 정렬 기능이 있는 것만
                    var orderNow = Number($(this).find('[name="select_column_order[]"]').val());
                    var className = $(this).find('a.sort').attr('class');
                    if (className.indexOf('asc') !== -1 || className.indexOf('desc') !== -1) {
                        if (orderDel < orderNow) {   // 큰 수만 1개씩 내림
                            $(this).find('[name="select_column_order[]"]').val(orderNow - 1);
                            $(this).find('a.sort span.order').text(orderNow - 1);
                        }
                    }
                }
            });
        }
    });

    // 정렬정보 초기화
    $('section.select-column a.reset').on('click', function() {
        if (confirm('현재 컬럼 설정을 초기화 하시겠습니까? 기본 설정된 컬럼 정보로 표시됩니다.')) {
            setCookie($pageId, "", -1);
            location.reload();
            //location.href = location.href;       // reload
        }
    });

    // 정렬정보 저장
    $('section.select-column a.save').on('click', function() {
        var setting = {};

        // 컬럼별 설정 정보 저장
        $('section.select-column ul.input-list li').each(function() {
            var show = $(this).find('[name="select_column[]"]').prop('checked') ? 'show' : 'hidden';
            var name = $(this).find('[name="select_column[]"]').val();
            var sort = ($(this).find('a.sort').length < 1) ? false : true;
            var sortType = (sort) ? $(this).find('[name="select_column_sort[]"]').val() : '';
            var sortOrder = (sort) ? parseInt($(this).find('[name="select_column_order[]"]').val()) : '';
            setting[name] = {};
            setting[name].show = show;
            setting[name].name = name;
            setting[name].sort = sort;
            setting[name].sort_type = sortType;
            setting[name].sort_order = sortOrder;
        });

        // 리스트 설정 정보 저장 (no 형태, 리스트 수, 엑셀반영여부)
        setting.set_no_sort = $(this).closest('section').find('[name="set_no_sort"]').val();
        setting.set_num_list = $(this).closest('section').find('[name="set_num_list"]').val();
        setting.set_excel_col = $(this).closest('section').find('[name="set_excel_col"]').val();

        //console.log(setting);

        if (confirm('현재 컬럼 설정을 저장하시겠습니까?')) {
            setCookie($pageId, JSON.stringify(setting), 30);
            location.reload();
            //location.href = location.href;       // reload
        }
    });
}

function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
    var expires = "expires="+ d.toUTCString();
    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for(var i = 0; i <ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}


$.fn.setSmartTable = function(options) {
    var settings = $.extend({
        maxHeight: 0
    }, options);

    this.each(function() {
        var $selector = $(this);
        var clonedTable = '';
        var tableWidth = 0;
        var numFixedCol = 0;
        var fixedWidth = 0;
        var timerId = '';

        if (settings.maxHeight > 0) {
            $(this).css({'max-height': settings.maxHeight + 'px'});
        }

        // 테이블 폭 설정
        $selector.find('colgroup col').each(function() {
            tableWidth += parseInt($(this).attr('data-width'));
        });
        $selector.find('> div.box, table:eq(0)').css({'width': tableWidth + 'px'});
        clonedTable = $selector.find('> div.box').html();

        // 고정폭 구하기
        $selector.find('> div.box > table:eq(0) colgroup col').each(function(i) {
            if ($(this).attr('data-fixed') === 'fixed') {
                numFixedCol++;
                fixedWidth += parseInt($(this).attr('data-width'));
            }
        });

        // 고정 header 삽입
        addFixedHeader();
        function addFixedHeader() {
            $selector.find('> div.box').append(clonedTable);
            $selector.find('table.list:eq(1)').addClass('fixed-header');
            $selector.find('table.list.fixed-header').find('tbody, tfoot').remove();
        }

        // 고정 left 삽입
        addFixedLeft();
        function addFixedLeft() {
            $selector.find('> div.box').append('<div class="fixed-left"></div>');
            $selector.find('> div.box div.fixed-left').append(clonedTable).css({'width': fixedWidth + 'px'});
            $selector.find('table.list:eq(2)').addClass('fixed-left');
            $selector.find('> div.box div.fixed-left table tbody tr').each(function() {
                $(this).find('td').each(function(i) {
                    if (i < numFixedCol) {  // 고정된 컬럼인 경우
                        $(this).find('input, select, textarea').addClass('form-fixed-left');
                    } else {
                        $(this).find('input, select, textarea').prop({'disabled': true});     // 숨겨진 내부의 form disabled 처리
                    }
                });
            });

            $selector.find('> div.box table.list:eq(0) tbody tr').each(function() {
                $(this).find('td').each(function(i) {
                    if (i < numFixedCol) {  // 고정된 컬럼인 경우
                        $(this).find('input, select, textarea').addClass('under-fixed-left').prop({'disabled': true});
                        $(this).find('input[type="hidden"]').remove();
                    }
                });
            });

            // $selector.find('table.list.fixed-left td:not(:nth-child(1))').find('input, select, textarea').remove();
            // 추가된 테이블 tbody의 form 요소 모두 제거 (list 저장시 문제), 단 첫 컬럼에 있는 checkbox는 놔둠(2중 동작, 체크 필요)
            // $selector.find('table.list:eq(0) tbody tr td:nth-child(1) input').remove();
            // 원본 테이블의 첫 컬럼의 input 요소(checkbox, hidden 등) 모두 제거
        }

        // 고정 left top 삽입
        addFixedLeftTop();
        function addFixedLeftTop() {
            $selector.find('> div.box').append('<div class="fixed-left-top"></div>');
            $selector.find('> div.box div.fixed-left-top').append(clonedTable).css({'width': fixedWidth + 'px'});
            $selector.find('table.list:eq(3)').addClass('fixed-left-top');
            $selector.find('table.list.fixed-left-top').find('tbody, tfoot').remove();
        }

        // fixed 영역 고정
        checkFixedTablePosition();
        $selector.on('scroll', function() {
            checkFixedTablePosition();
        });
        function checkFixedTablePosition() {
            clearTimeout(timerId);
            var scrollTop = $selector.scrollTop();
            var scrollLeft = $selector.scrollLeft();
            $selector.find('table.list.fixed-header').css({'top': scrollTop + 'px', 'transition': 'none', 'opacity': 0});
            $selector.find('div.fixed-left').css({'left': scrollLeft + 'px', 'transition': 'none', 'opacity': 0});
            $selector.find('div.fixed-left-top').css({'left': scrollLeft + 'px', 'top': scrollTop + 'px', 'transition': 'none', 'opacity': 0});
            timerId = setTimeout(function() {
                $('table.list.fixed-header, div.fixed-left, div.fixed-left-top').css({'transition': 'opacity 0.3s', 'opacity': 1});
            }, 200);
        }

        // 틀이 더 큰 경우에는 100%로 전환 (왼쪽 fixed 내용 표시/삭제)
        // 안보이는 form은 disabled 처리하여 데이타가 전송되지 않도록 수정
        resetTableWidth();
        $(window).on('resize', function() {
            resetTableWidth();
        });
        function resetTableWidth() {
            var boxWidth = $selector.width();
            if (boxWidth >= tableWidth || ($selector.find('> div.box table.list:eq(0) p.no-data').length > 0)) {    // box가 더 크거나, 내용이 없는 경우
                $selector.find('> div.box, table.list').css({'width': '100%'});
                $selector.find('table.list.fixed-left, table.list.fixed-left-top').css({'display': 'none'});
                $selector.find('table.list .form-fixed-left').prop({'disabled': true});
                $selector.find('table.list .under-fixed-left').prop({'disabled': false});
            } else {
                $selector.find('> div.box, table.list').css({'width': tableWidth + 'px'});
                $selector.find('table.list.fixed-left, table.list.fixed-left-top').css({'display': 'table'});
                $selector.find('table.list .form-fixed-left').prop({'disabled': false});
                $selector.find('table.list .under-fixed-left').prop({'disabled': true});
            }
        }

        // 전체 및 개별 체크시 checkbox 연동
        $('[name="select_all"]').on('change', function() {
            if ($(this).prop('checked') === true) {
                $('[name="check_seq[]"], [name="select_all"]').prop({'checked': true});
            } else {
                $('[name="check_seq[]"], [name="select_all"]').prop({'checked': false});
            }
        });
        $('[name="check_seq[]"]').on('change', function() {
            var index = $(this).closest('table').find('[name="check_seq[]"]').index($(this));
            if ($(this).prop('checked') === true) {
                $selector.find('table.list.fixed-left [name="check_seq[]"]:eq(' + index + '), table.list:eq(0) [name="check_seq[]"]:eq(' + index + ')').prop({'checked': true});
            } else {
                $selector.find('table.list.fixed-left [name="check_seq[]"]:eq(' + index + '), table.list:eq(0) [name="check_seq[]"]:eq(' + index + ')').prop({'checked': false});
            }
        });

        // select 포함시 값 선택
        $selector.find('select').each(function() {
            var value = $(this).attr('data-value');
            $(this).find('option').each(function() {
                if (value === $(this).attr('value')) {
                    $(this).prop({'selected': true});
                } else {
                    $(this).prop({'selected': false});
                }
            });
        });

        // checkbox 포함시 check 여부 (value 속성값과 data-value 속성값이 같으면 체크)
        $selector.find('input[type="checkbox"]').each(function() {
            var value1 = $(this).val();
            var value2 = $(this).attr('data-value');
            if (value1 === value2) {
                $(this).prop({'checked': true});
            }

            $(this).on('change', function() {
                if ($(this).prop('checked') === true) {
                    $(this).next().val(value1);
                } else {
                    $(this).next().val('N');
                }
            });
        });

    });   // end-of-each
}



// calendar UI
function setDatepicker() {
    // 이벤트(위임 방식 적용 / jquery data 활용(attr 사용시 적용 후 복사된 요소에 미적용))
    $(document).on('focusin', 'input.year, input.month, input.date, input.datetime-hour, input.datetime-min', function() {
        var $inputDate = $(this);
        if ($(this).data('isApplied') !== 'true') {
            $(this).data({'isApplied': 'true'});
            applyDatepicker($inputDate);
            setTimeout(function() {$inputDate.trigger('focus');}, 100);
        }
    });
    $(document).on('click', 'input.year ~ a.btn, input.month ~ a.btn, input.date ~ a.btn, input.datetime-hour ~ a.btn, input.datetime-min ~ a.btn', function() {
        $(this).parent().find('input').trigger('focus');
    });

    function applyDatepicker($selector) {
        var autoClose = true;
        var date = new Date();
        var minuteStep = 5;      // 분 기본 Step 5분
        var readonly = $selector.attr('data-readonly');
        var setYear = date.getFullYear();
        var setMonth = date.getMonth();
        var setDate = date.getDate();
        var setHour = '00';
        var setMin = '00';

        //console.log($selector.val());
        if ($selector.val() !== '') {
            setYear = parseInt($selector.val().substr(0, 4));
            setMonth = parseInt($selector.val().substr(5, 2) ? $selector.val().substr(5, 2) : 1) - 1;
            setDate = parseInt($selector.val().substr(8, 2) ? $selector.val().substr(8, 2) : 1);
            setHour = parseInt($selector.val().substr(11, 2) ? $selector.val().substr(11, 2) : 0);
            setMin = parseInt($selector.val().substr(14, 2) ? $selector.val().substr(14, 2) : 0);
        }
        // console.log(setYear + ' / ' + setMonth + ' / ' + setDate + ' / ' + setHour + ' / ' + setMin);


        $selector.prop({'readonly': true});
        if (readonly !== undefined && readonly === 'false') {
            $selector.prop({'readonly': false});
        }

        if ($selector.attr('class').indexOf('year') !== -1) {
            $selector.attr({'maxlength': 4, 'data-min-view': 'years', 'data-view': 'years', 'data-date-format': 'yyyy'});
        } else if ($selector.attr('class').indexOf('month') !== -1) {
            $selector.attr({'maxlength': 7, 'data-min-view': 'months', 'data-view': 'months', 'data-date-format': 'yyyy-mm'});
        } else if ($selector.attr('class').indexOf('date') !== -1) {
            $selector.attr({'maxlength': 10});
            if ($selector.attr('class').indexOf('datetime-hour') !== -1) {
                autoClose = false;
                $selector.attr({'maxlength': 16, 'data-timepicker': 'true', 'data-time-format': 'hh:00'});
                if ($selector.attr('data-step') !== undefined) {
                    minuteStep = Number($selector.attr('data-step'));   // step 설정 가능
                }
            } else if ($selector.attr('class').indexOf('datetime-min') !== -1) {
                autoClose = false;
                $selector.attr({'maxlength': 16, 'data-timepicker': 'true', 'data-time-format': 'hh:ii'});
                if ($selector.attr('data-step') !== undefined) {
                    minuteStep = Number($selector.attr('data-step'));   // step 설정 가능
                }
            }
        }

        var datepicker = $selector.datepicker({
            language: 'ko',
            autoClose: autoClose,
            startDate: new Date(setYear, setMonth, setDate, setHour, setMin, 0, 0),  // 초기 설정
            minutesStep: minuteStep,    // 분 단위
            clearButton: true,
            onHide: function() {$selector.trigger('change');}   // 달력이 사라질때 해당 input에 change 이벤트 발생
            //minDate: new Date()
        }).data('datepicker');

        // console.log(datepicker);
        datepicker.selectDate(new Date(setYear, setMonth, setDate, setHour, setMin, 0, 0));

        autoFormatDateTime($selector);
    }

    // 자동포맷 적용
    function autoFormatDateTime($selector) {
        // 이벤트(포맷 자동변경)
        $selector.on('keyup', function(e) {
            // backspace이면 동작하지 않음(수정시)
            if (e.keyCode === 8) return false;

            var maxLength = Number($(this).attr('maxlength'));
            var text = $(this).val();
            var pattern = /[^\d]/g;  // 입력허용값(숫자만)
            var newText = text.replace(pattern, '');  // 숫자이외의 기호는 없앰
            var newTextLength = newText.length;
            // Dash 자동 삽입
            if (newTextLength > 4) {
                newText = newText.slice(0, 4) + '-' + newText.slice(4);
            }
            if (newTextLength > 6){
                newText = newText.slice(0, 7) + '-' + newText.slice(7);
            }
            if (newTextLength > 8){
                newText = newText.slice(0, 10) + ' ' + newText.slice(10);
            }
            if (newTextLength > 10){
                newText = newText.slice(0, 13) + ':' + newText.slice(13);
            }
            // 최대값을 넘는 경우 삭제
            if (newText.length > maxLength) {
                newText = newText.slice(0, maxLength);
            }
            // 반영
            $(this).val(newText);  // 현재 요소만
        });
    }
}

// 윈도우 팝업
function openPopup(url, name, width, height, top, left) {  // 앞의 4개 파라미터 필수, left/top 선택(없으면 0)
    if (top === undefined) top = 0;
    if (left === undefined) left = 0;
    window.open(url, name, 'toolbar = no, scrollbars = yes, resizable = no, menubar = no, status = no, titlebar = no, top = ' + top + ', left = ' + left + ', width = ' + width + ', height = ' + height);
}


// 입력값 관리 (페이지)
function setFormValue(selector, dataJson) {
    var dataObj = JSON.parse(dataJson);

    // 검색 내용에 따른 상태정보 반영
    for (var key in dataObj) {
        var $formElement = (Array.isArray(dataObj[key]) === true) ? $(selector).find('[name="' + key + '[]"]') : $formElement = $(selector).find('[name="' + key + '"]');
        var formType = '';
        var value = '';

        // 폼이 없으면 다음으로 넘김
        if ($formElement.length < 1) continue;

        // 여러 개(배열)인 경우 첫번째 요소만 체크
        // checkbox, radio 버튼의 경우 기본적으로 동일이름의 요소가 여러개 존재 (리스트 내부의 경우에도 form name이 같을 수 없음 : 1개의 set으로 간주)
        formType = $formElement.eq(0).prop('tagName').toLowerCase();
        if (formType === 'input') formType += '-' + $formElement.eq(0).attr('type');
        value = dataObj[key];   // 모든 값은 다 string (.toString() 불필요)
        // console.log(key + ' [' + formType + '] : ' + value + ' / count : ' +  $formElement.length);


        switch (formType) {
            case 'input-text':
            case 'input-hidden':
            case 'textarea':
                if (Array.isArray(value) === true) {
                    $formElement.each(function(i) {
                        if (value[i] !== '' && $formElement.prop('disabled') === true) $formElement.prop({'disabled': false});  // 값이 있는 경우 diabled 요소 해제
                        $(this).val(value[i]);
                    });
                } else {
                    if (value !== '' && $formElement.prop('disabled') === true) $formElement.prop({'disabled': false});  // 값이 있는 경우 diabled 요소 해제
                    $formElement.val(value);
                }
                break;
            case 'input-checkbox':
                $formElement.prop({'checked': false});  // 모두 해제 후 value가 같은 것만 다시 체크
                var checkValue = value;
                if (typeof(value) === 'string') {
                    var checkValue = value.split(',');   // 3,4,5 string 형태로 전달되면 배열로 변환 (view/edit) / 1개인 경우(, 없어도) 배열[0] 변환
                } else {
                    var checkValue = value;              // 배열인 경우 그대로 사용(search)
                }
                for (var i  = 0; i < checkValue.length; i++) {
                    $formElement.each(function() {
                        if ($(this).val() ===  checkValue[i].toString()) $(this).prop({'checked': true});
                    });
                }
                break;
            case 'input-radio':
                $formElement.each(function() {
                    if ($(this).val() ===  value.toString()) $(this).prop({'checked': true});
                });
                break;
            case 'select':
                if (Array.isArray(value) === true) {
                    $formElement.each(function(i) {
                        $selectForm = $(this);
                        $selectForm.find('option').each(function() {
                            if (value[i] === undefined) value[i] = '';
                            if ($(this).val() ===  value[i].toString()) $(this).prop({'selected': true});
                        });

                        // select + input 인 경우
                        if (value[i] !== '' && $formElement.prop('disabled') === true) $formElement.prop({'disabled': false});  // 값이 있는 경우 diabled 요소 해제
                        $(this).val(value[i]);
                    });
                } else {
                    $formElement.find('option').each(function() {
                        if ($(this).val() === value.toString()) $(this).prop({'selected': true});
                    });
                }
                break;
            default:
        }
    }

    // 빈 체크박스 채우기 (검색 form에만 적용)
    if (selector === '[name="form_search"]') {
        $(selector).find('ul.input-list').each(function() {
            var numChecked = $(this).find('input:checked').length;
            if (numChecked === 0) {
                $(this).find('input[type="checkbox"]').prop({'checked': true});
            }
        });
    }
}


// 리스트 UI
$.fn.setListUI = function(options) {
    var settings = $.extend({
        initNumList: 0,                            // 초기 기본으로 표시할 리스트 수
    }, options);

    this.each(function() {
        var $selector = $(this).parent();  // 대상 table의 부모
        var $selectorInsert = '';
        var $listNoInfo = '';
        var $listSample = '';
        var numCol = 0;
        var listTag = $selector.find('.sample-list').prop('tagName').toLowerCase();

        // 컬럼수
        $selector.find('thead tr:eq(0) th').each(function() {
            if ($(this).attr('colspan') !== undefined || parseInt($(this).attr('colspan')) > 0) {
                numCol += parseInt($(this).attr('colspan'));
            } else {
                numCol++;
            }
        });
        // 기준 list 생성(no-info / sample)
        if (listTag === 'tr') {  // tr 기준으로 list 구성
            $selectorInsert = $selector.find('table tbody');
            $listNoInfo = '<tr class="no-info"><td colspan="' + numCol + '"><p class="no-data"><i class="fas fa-exclamation-triangle"></i> 입력된 내용이 없습니다.</p></td></tr>';
            $listSample = '<tr>' + $selector.find('.sample-list').html() + '</tr>';
        } else if (listTag === 'tbody') {  // tbody 기준으로 list 구성
            $selectorInsert = $selector.find('table');
            $listNoInfo = '<tbody class="no-info"><tr><td colspan="' + numCol + '"><p class="no-data"><i class="fas fa-exclamation-triangle"></i> 입력된 내용이 없습니다.</p></td></tr></tbody>';
            $listSample = '<tbody>' + $selector.find('.sample-list').html() + '</tbody>';
        }

        // 초기화 (기존데이터 있거나 기본적으로 n개 표시하는 경우도 있음)
        $selector.find('.sample-list').remove();
        if (settings.initNumList > 0) {
            addList(settings.initNumList);
        } else {
            delList();
        }

        $selector.on('click', 'a.add', function() {
            addList();
        });
        $selector.on('click', 'a.up', function() {
            if (Number($(this).closest(listTag).find('input[data-type="order"]').val()) === 1) {
                alert('가장 위에 위치한 리스트입니다.');
            } else {
                var selectedInfo = getSelectedInfo($(this).closest(listTag));
                var clonedList = $(this).closest(listTag).clone();
                $(this).closest(listTag).prev().before(clonedList);
                $(this).closest(listTag).remove();
                resetIndex();
                setSelectedInfo(selectedInfo);
            }
        });
        $selector.on('click', 'a.down', function() {
            if (Number($(this).closest(listTag).find('input[type="hidden"]:eq(1)').val()) === $selectorInsert.find('> ' + listTag).length) {
                alert('가장 아래에 위치한 리스트입니다.');
            } else {
                var selectedInfo = getSelectedInfo($(this).closest(listTag));
                var clonedList = $(this).closest(listTag).clone();
                $(this).closest(listTag).next().after(clonedList);
                $(this).closest(listTag).remove();
                resetIndex();
                setSelectedInfo(selectedInfo);
            }
        });
        $selector.on('click', 'a.del', function() {
            $(this).closest(listTag).remove();
            resetIndex();
            if ($selectorInsert.find('> ' + listTag).length < 1) {  // 리스트 내용이 없는 경우에 표시
                delList();
            }
        });
        $selector.on('click', 'a.del-all', function() {
            delList();
        });

        function delList() {
            $selectorInsert.find('> ' + listTag).remove();
            if (listTag === 'tr') {
                $selectorInsert.append($listNoInfo);
            } else if (listTag === 'tbody') {
                if ($selector.find('tfoot').length > 0) {
                    $selector.find('tfoot').before($listNoInfo);
                } else {
                    $selectorInsert.append($listNoInfo);
                }
            }
        }

        function addList(n) {       // n개 리스트 추가
            if (n === undefined) n = 1;
            $selector.find('.no-info').remove();
            for (var i = 0; i < n; i++) {
                if (listTag === 'tr') {
                    $selectorInsert.append($listSample);
                } else if (listTag === 'tbody') {
                    if ($selector.find('tfoot').length > 0) {
                        $selector.find('tfoot').before($listSample);
                    } else {
                        $selectorInsert.append($listSample);
                    }
                }
            }
            resetIndex();
        }

        function resetIndex() {
            $selectorInsert.find('> ' + listTag).each(function(i) {
                $(this).find('span.index').text(i + 1);
                $(this).find('input[data-type="order"]').val(i + 1);      // data-type 속성이 order 인 경우
                $(this).find('[id]').each(function() {
                    var attrValue = $(this).attr('id');
                    var newValue = attrValue.replace(/-\d+$/gi, '-' + (i + 1));
                    $(this).attr({'id': newValue});
                });
            });
        }

        // 해당 셀렉터 내부의 정보 저장
        function getSelectedInfo(selector) {  // 첨부파일이 있는 경우도 가능한가?
            var selectedInfo = [];
            $(selector).attr({'data-temp': 'move'});   // 이동 내용 표시
            $(selector).find('select, input[type="radio"], input[type="checkbox"]').each(function(i) {
                var formType = '';
                formType = $(this).prop('tagName').toLowerCase();
                if (formType === 'input') formType += '-' + $(this).attr('type');

                switch (formType) {
                    case 'select':
                        selectedInfo[i] = $(this).find('option').index($(this).find('option:selected'));
                        break;
                    case 'input-radio':
                    case 'input-checkbox':
                        selectedInfo[i] = $(this).prop('checked');
                        break;
                    default: selectedInfo[i] = 'no';
                }
            });
            //console.log(selectedArray);
            return selectedInfo;
        }

        // 해당 정보 적용
        function setSelectedInfo(info) {  // move 표시된 list에 값 반영
            var selectedInfo = info;
            $selector.find('[data-temp="move"]').find('select, input[type="radio"], input[type="checkbox"]').each(function(i) {
                var formType = '';
                formType = $(this).prop('tagName').toLowerCase();
                if (formType === 'input') formType += '-' + $(this).attr('type');
                switch (formType) {
                    case 'select':
                        $(this).find('option:eq(' + selectedInfo[i] + ')').prop({'selected': true});
                        break;
                    case 'input-radio':
                    case 'input-checkbox':
                        $(this).prop({'checked': selectedInfo[i]});
                        break;
                }
            });
            $selector.find('[data-temp="move"]').removeAttr('data-temp');
            //console.log(selectedArray);
        }

    });    // end of each
}


// 입력값 자동 필터링
function setInputAutoFilter() {
    // 정수(+)
    $(document).on('input', 'input[data-type="int-plus"]', function() {
        var valueNow = $(this).val();
        var valueNew = '';
        valueNew = valueNow.replace(/[^0-9]/gi, '');  // 숫자 이외 입력불가 (g 모든(global) 패턴체크, i 대소문자 구분없이 체크)
        //valueNew = valueNew.replace(/^0+/gi, '');   // 처음부분에 있는 0 모두 제거(불필요)
        if (RegExpResults = /^0(.)/gi.exec(valueNew)) {  // 0 입력이 아무것도 입력 불가
            valueNew = valueNew.substr(0, 1);
        }
        valueNew = valueNew.replace(/\B(?=(\d{3})+(?!\d))/g, ',');  // 천단위 콤마 적용
        $(this).val(valueNew);
    });

    // 정수(+/-)
    $(document).on('input', 'input[data-type="int"]', function() {
        var valueNow = $(this).val();
        var valueNew = '';
        var RegExpResults = '';
        valueNew = valueNow.replace(/[^0-9-]/gi, '');  // 숫자와 음수부호(-) 이외 입력불가 (g 모든(global) 패턴체크, i 대소문자 구분없이 체크)
        //valueNew = valueNew.replace(/^0+/gi, '');   // 처음부분에 있는 0 모두 제거(불필요)
        if (RegExpResults = /^0(.)/gi.exec(valueNew)) {  // 0 입력이 아무것도 입력 불가
            valueNew = valueNew.substr(0, 1);
        }
        if (RegExpResults = /-(0)/gi.exec(valueNew)) {  // - 바로 다음의 0 제거
            valueNew = valueNew.substr(0, 1);
        }
        if (RegExpResults = /.{1}(-)/gi.exec(valueNew)) {  // 중간의 - 부호 제거
            valueNew = valueNew.substr(0, valueNew.length - 1);  // 마지막 입력값 제거
        }
        valueNew = valueNew.replace(/\B(?=(\d{3})+(?!\d))/g, ',');  // 천단위 콤마 적용
        $(this).val(valueNew);
    });

    // 실수(+)
    $(document).on('input', 'input[data-type="real-plus"]', function() {
        var valueNow = $(this).val();
        var valueNew = '';
        var valueSeparate = '';
        var RegExpResults = '';
        valueNew = valueNow.replace(/[^0-9.]/gi, '');  // 숫자 및 소수점(.) 이외 입력불가 (g 모든(global) 패턴체크, i 대소문자 구분없이 체크)
        if (RegExpResults = /\..*\./gi.exec(valueNew)) {   // 소수점은 1번만 입력 가능
            valueNew = valueNew.substr(0, valueNew.length - 1);  // 마지막 입력값 제거
        }
        if (RegExpResults = /^\./gi.exec(valueNew)) {  // 첫 자리에는 .은 입력불가
            valueNew = valueNew.substr(0, valueNew.length - 1);  // 마지막 입력값 제거
        }
        if (RegExpResults = /^0[^.]/gi.exec(valueNew)) {  // 첫 0 이후에는 소수점만 입력가능
            valueNew = valueNew.substr(0, valueNew.length - 1);  // 마지막 입력값 제거
        }
        // 천단위 콤마적용
        valueSeparate = valueNew.toString().split('.');
        valueSeparate[0] = valueSeparate[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
        valueNew = valueSeparate[0] + ((valueSeparate[1] === undefined) ? '' : '.' + valueSeparate[1]);  // 정수인 경우 소숫점 부분 없음
        $(this).val(valueNew);
    });

    // 실수(+/-)
    $(document).on('input', 'input[data-type="real"]', function() {
        var valueNow = $(this).val();
        var valueNew = '';
        var valueSeparate = '';
        var RegExpResults = '';
        valueNew = valueNow.replace(/[^0-9.-]/gi, '');  // 숫자 및 소수점(.), 음수부호(-) 이외 입력불가 (g 모든(global) 패턴체크, i 대소문자 구분없이 체크)
        if (RegExpResults = /\..*\./gi.exec(valueNew)) {   // 소수점은 1번만 입력 가능
            valueNew = valueNew.substr(0, valueNew.length - 1);  // 마지막 입력값 제거
        }
        if (RegExpResults = /^\./gi.exec(valueNew)) {  // 첫 자리에는 .은 입력불가
            valueNew = valueNew.substr(0, valueNew.length - 1);  // 마지막 입력값 제거
        }
        if (RegExpResults = /^-?0[^.]/gi.exec(valueNew)) {  // 첫 0(혹은 -0) 이후에는 소수점만 입력가능
            valueNew = valueNew.substr(0, valueNew.length - 1);  // 마지막 입력값 제거
        }
        if (RegExpResults = /.{1}(-)/gi.exec(valueNew)) {  // 중간의 - 부호 제거
            valueNew = valueNew.substr(0, valueNew.length - 1);  // 마지막 입력값 제거
        }
        // 천단위 콤마적용
        valueSeparate = valueNew.toString().split('.');
        valueSeparate[0] = valueSeparate[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
        valueNew = valueSeparate[0] + ((valueSeparate[1] === undefined) ? '' : '.' + valueSeparate[1]);  // 정수인 경우 소숫점 부분 없음
        $(this).val(valueNew);
    });
}


// 주소검색 UI (다음 API)
function getPostcode(idPostNum, idAddress1, idAddress2) {
    new daum.Postcode({
        oncomplete: function(data) {
            // 각 주소의 노출 규칙에 따라 주소를 조합한다.
            // 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
            var fullAddr = ''; // 최종 주소 변수
            var extraAddr = ''; // 조합형 주소 변수

            // 사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
            if (data.userSelectedType === 'R') { // 사용자가 도로명 주소를 선택했을 경우
                fullAddr = data.roadAddress;

            } else { // 사용자가 지번 주소를 선택했을 경우(J)
                fullAddr = data.jibunAddress;
            }

            // 사용자가 선택한 주소가 도로명 타입일때 조합한다.
            if(data.userSelectedType === 'R'){
                //법정동명이 있을 경우 추가한다.
                if(data.bname !== ''){
                    extraAddr += data.bname;
                }
                // 건물명이 있을 경우 추가한다.
                if(data.buildingName !== ''){
                    extraAddr += (extraAddr !== '' ? ', ' + data.buildingName : data.buildingName);
                }
                // 조합형주소의 유무에 따라 양쪽에 괄호를 추가하여 최종 주소를 만든다.
                fullAddr += (extraAddr !== '' ? ' ('+ extraAddr +')' : '');
            }

            // 우편번호와 주소 정보를 해당 필드에 넣는다.
            document.getElementById(idPostNum).value = data.zonecode; //5자리 새우편번호 사용
            document.getElementById(idAddress1).value = fullAddr;

            // 커서를 상세주소 필드로 이동한다.
            document.getElementById(idAddress2).focus();
        }
    }).open();
}


// tinymce editor 적용
function setEditor() {
    tinymce.init({
        selector: 'textarea.editor',
        plugins: [
          'advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker',
          'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking',
          'save table directionality emoticons template paste'
        ],
        //content_css: 'css/content.css',
        content_style: 'p {font-family: "나눔고딕", NanumGothic; margin: 5px; font-size: 13px; line-height: 1.4em;}',
        toolbar: 'print preview newdocument undo redo | fontselect | fontsizeselect | forecolor backcolor bold italic underline strikethrough | link image media charmap code',
        font_formats: '나눔고딕=나눔고딕,NanumGothic;Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats',
        fontsize_formats: '11px 12px 14px 16px 18px 24px 36px 48px',
        language: 'ko_KR',
        images_upload_url: '/admin/editor-image-upload',
        images_upload_base_path: '/storage/editor',
        relative_urls : false,
        images_upload_credentials: true,
        image_dimensions: false,
        setup: function (editor) {
            editor.on('change', function() { editor.save(); });
        }
    });
}


