
/*=================================================================================================
 * MARI HOME   ---------------"--<@
 *-------------------------------------------------------------------------------------------------
 * @코드 명칭 : 마리 멀티 업로더 ( 가제 ) 
 * @요약 정보 : 업로더에 필요한 작은 기능 함수들 모음
 * @파일 이름 : multi_uploader_util.js
 * @웹용 주소 : 글쓰기등 업로드가 필요한 곳, 파일 탐색기와 같이 파일 미리보기가 필요한 곳에서 외부 파일(js파일)로 호출됨 
 * @호출 파일 : multi_uploader_code.js에 스프레드 연산자로 등록되어 각 요소에서 호출됨
 * @서버 위치 : 마리홈 ( ),  그누보드 ( plugin/multi_uploader/ )
 * @소스 제작 : repter ( 예뜨락 ), 홈페이지 ( https://marihome.net ) 
 * @제작 날짜 : 2023.08.01 ( 최초 제작 시작일 )
 * @소스 설명 : 업로더에 필요한 기능성 메서드등을 만들어 필요한 곳에서 적절히 사용한다
 * @기타 사항 : 당분간 필요한 업그레이드를 통해서 소스 변경이 자주 일어날 수 있습니다 
 *           자바스크립트 형태 : 객체 리터럴 ( Object Literal )
 *           변수 => 프로퍼티 (Property)   : 프로퍼티 값
 *           함수 => 메서드 (Method)
 * @최종 수정 : 2025.03.26
 * @버전 명시 : 0.9
 *-------------------------------------------------------------------------------------------------
 * Copyright 2023. repter All Rights Reserved.
**===============================================================================================*/ 

// 글로벌 변수 지정
if(typeof(window.MARI) === "undefined") { (function(global) { global.MARI = {}; })(window || global); }
if(typeof(window.MARI.file_uploder) === "undefined") { (function(global) { global.MARI.file_uploder = {}; })(window || global); }
if(typeof(window.MARI.file_uploder.util) === "undefined") { // 등록이 되지 않았다면 등록

    // 파일 업로더에 관련된 유틸
	MARI.file_uploder.util = {
		/*=========================================================================================
		 * 필요한 변수 설정
		**---------------------------------------------------------------------------------------*/ 

        /**
         * shape1과 같이 shape와 숫자로 이뤄졌는지
        */
        isShapeTypeValid : function(shapeType) {
            return /^shape\d+$/.test(shapeType);
        },


		/*=========================================================================================
		 * 0. 이벤트 관련
		**---------------------------------------------------------------------------------------*/

        /**
         * 이벤트 처리 함수
        */
        addEvent : function (selector, event, handler) {
            document.querySelectorAll(selector).forEach(element => 
                element.addEventListener(event, handler)
            );
        },


        /**
         * 이벤트 막기 및 처리
        */ 
        preventDefaultActions : function (e) {
            e.preventDefault();
            e.stopPropagation();
        },


		/*=========================================================================================
		 * 0. 쿠키, 로컬스토리지 관련
		**---------------------------------------------------------------------------------------*/
        
        /**
         * 로컬 스토리지에 데이터 저장
        */
        saveToLocalStorage : function(key, value) { 
            // 현재 로컬 스토리지에 저장된 데이터를 불러오기
            const existingData = JSON.parse(localStorage.getItem(key)) || {};

            // 기존 데이터에 새 데이터를 병합 (기존 데이터 수정됨)
            Object.assign(existingData, value);

            // 병합된 데이터를 다시 로컬 스토리지에 저장
            localStorage.setItem(key, JSON.stringify(existingData));
        },


        /**
         * 로컬 스토리지에서 데이터 가져오기
        */ 
        getFromLocalStorage : function(key) {
            const data = localStorage.getItem(key);
            return data ? JSON.parse(data) : null;
        },


		/*=========================================================================================
		 * 0. 시간 관련
		**---------------------------------------------------------------------------------------*/
        
        /**
         * 현재 시간 가져오기
        */ 
        now_time : function(obj_type) {
            /*===================================
             * 현재 시간을 원하는 형식으로 반환하는 함수
             * obj_type => 반환할 시간 형식 지정
             *   - 'Ymd'       => 'YYYY-MM-DD' 형식
             *   - 'Ymd_His'   => 'YYYY-MM-DD HH:mm:ss' 형식
             *   - 'ymdhis'    => 'YYYYMMDDHHmmss' 형식
             *   - 'His'       => 'HH:mm:ss' 형식
             *   - 'H : i'     => 'HH:mm' 형식
             *   - 'nowTime'   => Date 객체로 현재 시각 반환
             *   - 'now_sec'   => 'HHmmssSSS' 형식으로 밀리초까지 포함한 시간
             *   - 기본값     => Date 객체로 오늘 날짜 반환
             *
             * 사용 예시:
             * now_time('Ymd');      -> '2025-02-22' 반환
             * now_time('Ymd_His');  -> '2025-02-22 14:35:45' 반환
             * now_time('nowTime');  -> 현재 시간에 대한 Date 객체 반환
             * now_time('now_sec');  -> '143545123'와 같은 형식으로 밀리초 포함 반환
             *---------------------------------*/
            var now = new Date(); // 현재 시간 객체 생성
            var year = now.getFullYear();          // 연도
            var month = now.getMonth();             // 월 (0~11 범위, 주의)
            var day = now.getDate();                // 일
            var hour = now.getHours();              // 시
            var minute = now.getMinutes();          // 분
            var second = now.getSeconds();          // 초
            var millisec = now.getMilliseconds();   // 밀리초
            var nowDate = new Date(year, month, day); // 오늘
            var nowTime = new Date(year, month, day, hour, minute, second); // 현재 시간
            var month2 = (10 > month) ? '0' + (parseInt(month) + 1) : (parseInt(month) + 1); // 1~12월을 두 자릿수로 표시
            var day2 = (10 > day) ? '0' + day : day; // 일 두 자릿수로 표시
            var hour2 = (10 > hour) ? '0' + hour : hour; // 시 두 자릿수로 표시
            var minute2 = (10 > minute) ? '0' + minute : minute; // 분 두 자릿수로 표시
            var second2 = (10 > second) ? '0' + second : second; // 초 두 자릿수로 표시

            // obj_type에 맞는 형식으로 반환값 설정
            if (obj_type == 'Ymd') {
                var return_data = year + '-' + month2 + '-' + day2; // 'YYYY-MM-DD'
            }
            else if (obj_type == 'Ymd_His') {
                var return_data = year + '-' + month2 + '-' + day2 + ' ' + hour2 + ':' + minute2 + ':' + second2; // 'YYYY-MM-DD HH:mm:ss'
            }
            else if (obj_type == 'ymdhis') {
                var return_data = year + '' + month2 + '' + day2 + '' + hour2 + '' + minute2 + '' + second2; // 'YYYYMMDDHHmmss'
            }
            else if (obj_type == 'His') {
                var return_data = hour2 + ':' + minute2 + ':' + second2; // 'HH:mm:ss'
            }
            else if (obj_type == 'H : i') {
                var return_data = hour2 + ':' + minute2; // 'HH:mm'
            }
            else if (obj_type == 'nowTime') {
                var return_data = nowTime; // Date 객체 반환
            }
            else if (obj_type == 'now_sec') {
                var return_data = hour2 + '' + minute2 + '' + second2 + '' + millisec; // 'HHmmssSSS' (밀리초 포함)
            }
            else {
                var return_data = nowDate; // 기본적으로 Date 객체 반환
            }

            return return_data; // 최종 반환 값

        },


        /**
         * 날짜 포멧
        */ 
		dateFormat : function(data_times){
			let day   = ['일', '월', '화', '수', '목', '금', '토']; 
			let today = new Date(data_times);

			// 날짜 형식 처리
			let year = today.getFullYear();
			let month = (today.getMonth() + 1).toString().padStart(2, '0');
			let date = today.getDate().toString().padStart(2, '0');
			let dayOfWeek = day[today.getDay()];

			// 시간 형식 처리
			let hours = today.getHours();
			let period = hours >= 12 ? '오후' : '오전';
			hours = hours % 12 || 12;  // 0시는 12시로, 13시는 1시로 변환
			hours = hours.toString().padStart(2, '0');
			let minutes = today.getMinutes().toString().padStart(2, '0');

			// 최종 포맷
			let day_format = `${year}-${month}-${date}(${dayOfWeek}) ${period} ${hours}:${minutes}`;

			return day_format;

		},


        /**
         * 초를 시 분 초로 변경
        */
		convertSecondsToTime : function(seconds) {
			const hours      = String(Math.floor(seconds / 3600)).padStart(2, '0');
			const minutes    = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
			const reseconds  = String(seconds % 60).padStart(2, '0');

			return `${hours}시간 : ${minutes}분 : ${parseInt(reseconds)}초`;

		},


        /**
         * 초를 분 초로 변경
        */
		convertSecondsToMinutes : function (seconds) {
			const minutes = Math.floor(seconds / 60);
			const remainingSeconds = seconds % 60;
            
			return `${minutes}분 ${parseInt(remainingSeconds)}초`;

		},


        /**
         * 마이크로 초로 리턴
        */
		getCurrentTimeInMicroseconds : function() {
			const now = Date.now(); // 현재 시간을 밀리초로 반환 (UNIX 타임스탬프)
			const highResTime = performance.now(); // 페이지 로드 이후의 시간을 밀리초 단위로 반환 (정밀도 높음)
			// 밀리초를 마이크로초로 변환하고 두 값의 정밀도를 더해줌
			const microTime = BigInt(now) * 1000n + BigInt(Math.floor((highResTime % 1) * 1000));

			return microTime;

		},


		/*=========================================================================================
		 * 0. 파일 사이즈 변환 관련
		 *    비트( bit ) < 바이트( B ) < 킬로바이트( KB ) < 메가바이트( MB ) < 기가바이트( GB ) < 
		 *    테라바이트( TB ) < 페타바이트( PB ) < 엑사바이트( EB )
		 *    업로드시 킬로바이트로 받아짐
		**---------------------------------------------------------------------------------------*/

        /**
         * 메가바이트를 바이트로
        */
		mega_to_bite : function(mega_size, obj_type){
			return mega_size * 1024 * 1024;
		},


        // 파일 사이즈 환산시 기존 파일의 사이즈를 넣어두는 temp역할 바이트 단위로 들어가 있다
        // let total_size = obj_util.bite_to_mega(tempAddSize, 'add_size');
        tempSize      : 0,


        /**
         * 바이트를 킬로 바이트로
        */
		bite_to_killo : function(bite_size, obj_type){
			/*========================================
			 * 바이트를 킬로 바이트로 변경 소숫점 두자리까지 표기 
			**--------------------------------------*/
			let obj_code = MARI.file_uploder.code;
			let obj_util = MARI.file_uploder.util;  
            let tempSize = obj_util.tempSize;
            
			// 바이트를 킬로 바이트로 소숫점 3자리까지
			let kb_chg_size  = (parseInt(bite_size) / (1024)).toFixed(3); 
			let kb_chg_size2 = parseInt(kb_chg_size * 100) / 100; //킬로 바이트 소숫점 2자리까지만 남기고 버림 
			let return_size  = kb_chg_size2; // 변환 사이즈

			if(obj_type === 'add_size'){
				let total_size  = (tempSize + kb_chg_size2).toFixed(3); 
				let total_size2 = parseInt(total_size * 100) / 100; //킬로 바이트 소숫점 2자리까지만 남기고 버림 
				tempSize        = total_size2;// 현재의 사이즈를 변수에 넣어둔다
				return_size     = total_size2; //합산 사이즈
			}

			if(obj_type === 'minus_size'){
				let total_size  = (tempSize - kb_chg_size2).toFixed(3); 
				let total_size2 = parseInt(total_size * 100) / 100; //킬로 바이트 소숫점 2자리까지만 남기고 버림 
				tempSize        = total_size2; // 현재의 사이즈를 변수에 넣어둔다
				return_size     = total_size2; // 합산 사이즈
			}

			return return_size;
            
		},


        /**
         * 바이트를 메가 바이트로
        */
		bite_to_mega : function(bite_size, obj_type){
			/*========================================
			 * 바이트를 메가 바이트로 변경 소숫점 두자리까지 표기
			 * 기존 저장된 사이즈에 새로 받은 사이즈를 합한다
             * tempSize에는 계산시 바이트 단위로 들어가 있다
			**--------------------------------------*/
			let obj_util = MARI.file_uploder.util;
			let obj_code = MARI.file_uploder.code;  
            let tempSize = obj_util.tempSize;
            
			// 바이트를 메가 바이트로 소숫점 3자리까지
			let mb_chg_size  = (parseInt(bite_size) / (1024 * 1024)).toFixed(4); 
			let mb_chg_size2 = parseInt(mb_chg_size * 1000) / 1000; //메가 바이트 소숫점 3자리까지만 남기고 버림 
			let return_size  = mb_chg_size2; // 변환 사이즈

			if(obj_type === 'add_size'){ 
				let total_size     = (tempSize + mb_chg_size2).toFixed(4);
				let total_size2    = parseInt(total_size * 1000) / 1000; //메가 바이트 소숫점 3자리까지만 남기고 버림  
				tempSize           = total_size2;// 현재의 사이즈를 변수에 넣어둔다
				return_size        = total_size2; //합산 사이즈
			}

			if(obj_type === 'minus_size'){ 
				let total_size     = (tempSize - mb_chg_size2).toFixed(4); 
				let total_size2    = parseInt(total_size * 1000) / 1000; //메가 바이트 소숫점 3자리까지만 남기고 버림  
				tempSize = total_size2;// 현재의 사이즈를 변수에 넣어둔다
				return_size        = total_size2; //합산 사이즈
			}
 
			return return_size;

		},
 

        /**
         * 킬로 바이트를 메가 바이트로
        */
		killo_to_mega : function(file_size, obj_type){
			/*========================================
             * 킬로 바이트를 매개 변수로 넣음 
             * obj_util.killo_to_mega(20, 'add_size'); 
             * 20(kb)  => 0.02 (MB)[0.01953 (MB)] 
			 * 킬로 바이트를 메가 바이트로 변경 소숫점 두자리까지 표기
			 * 기존 저장된 사이즈에 새로 받은 사이즈를 합한다
			**--------------------------------------*/
			let obj_util = MARI.file_uploder.util;
			let obj_code = MARI.file_uploder.code;
            let tempSize = obj_util.tempSize;

			// 바이트를 메가 바이트로 소숫점 3자리까지
			let mb_chg_size  = (parseInt(file_size) / (1024)).toFixed(3); 
			let mb_chg_size2 = parseInt(mb_chg_size * 100) / 100; //메가 바이트 소숫점 2자리까지만 남기고 버림 
			let return_size  = mb_chg_size2; // 변환 사이즈

			if(obj_type === 'add_size'){
				let total_size  = ( tempSize + mb_chg_size2 ).toFixed(3); 
				let total_size2 = parseInt(total_size * 100) / 100; //메가 바이트 소숫점 2자리까지만 남기고 버림  
				tempSize        = total_size2;// 현재의 사이즈를 변수에 넣어둔다
 				return_size     = tempSize; //합산 사이즈 
			}

			if(obj_type === 'minus_size'){
				let total_size     = (tempSize - mb_chg_size2).toFixed(3); 
				let total_size2    = parseInt(total_size * 100) / 100; //메가 바이트 소숫점 2자리까지만 남기고 버림 
				tempSize = total_size2;// 현재의 사이즈를 변수에 넣어둔다
				return_size        = total_size2; //합산 사이즈
			}

			return return_size;

		},


		/*=========================================================================================
		 * 0. 인코딩 변환 관련
		**---------------------------------------------------------------------------------------*/

        /**
         * Base64로 인코딩된 문자열을 16진수로 변환
        */
        base64ToHex : function(str) {
            /*===================================
             * Base64로 인코딩된 문자열을 16진수로 변환하는 함수
             * str => Base64로 인코딩된 문자열
             *
             * 과정:
             * - Base64 인코딩된 문자열을 `atob()`를 사용하여 디코딩
             * - 디코딩된 결과 문자열의 각 문자를 해당하는 유니코드 값으로 변환하고 이를 16진수로 표현
             * - 16진수 값이 두 자릿수가 되지 않으면 앞에 '0'을 추가하여 두 자릿수를 유지
             * - 최종적으로 모든 16진수 값을 결합하여 반환
             *
             * 사용 예시:
             * base64ToHex('SGVsbG8='); // '48656C6C6F'
             *---------------------------------*/
            const raw = window.atob(str);
            let result = '';
            // 디코딩된 문자열의 각 문자에 대해 16진수 값으로 변환
            for (let i = 0; i < raw.length; i++) {
                const hex = raw.charCodeAt(i).toString(16);     // 각 문자의 유니코드 값을 16진수로 변환
                result += (hex.length === 2 ? hex : '0' + hex); // 2자리 16진수로 맞추어 추가
            }

            return result.toUpperCase(); // 대문자 형태의 16진수 반환

        },



		/*=========================================================================================
		 * 0. 윈도우 크기 변환 관련
		**---------------------------------------------------------------------------------------*/
        
        /**
         * 풀스크린으로 변환
        */
		full_screen : function(elem){
			let obj_code = MARI.file_uploder.code;
			let obj_util = MARI.file_uploder.util; 
 
			if(elem.requestFullscreen) {
				elem.requestFullscreen();
			} 
			else 
			if(elem.webkitRequestFullscreen) { /* Safari */
				elem.webkitRequestFullscreen();
			} 
			else 
			if(elem.msRequestFullscreen) { /* IE11 */
				elem.msRequestFullscreen();
			}

		},


        /**
         * 풀스크린에서 윈도우로, 윈도우 풀에서 윈도우 노멀로
        */
		normall_screen : function(){
			let obj_code = MARI.file_uploder.code;
			let obj_util = MARI.file_uploder.util;
			if(document.fullscreenElement) {//풀스크린에서 윈도우로
				if (document.exitFullscreen) {
					document.exitFullscreen();
				} else if (document.webkitExitFullscreen) { /* Safari */
					document.webkitExitFullscreen();
				} else if (document.msExitFullscreen) { /* IE11 */
					document.msExitFullscreen();
				}
			}
			else{//윈도우 풀에서 윈도우 노멀로
				return true;
			}

		},


        /**
         * 풀스크린, 윈도우 최대 크기로
        */
		full_window : function(){
			/*===========================================
			 * 파일 영역 전체창, 원래 크기로 보기
			**-----------------------------------------*/
			let use_full_screen = true; // true 풀스크린으로 false 윈도우 최대 크기로
			let obj_code = MARI.file_uploder.code;
			let obj_util = MARI.file_uploder.util;

            document.addEventListener('mousedown', function(event) { 
                if (event.target.closest('.file_zone_full_view')) {
                    let eq_idx       = obj_code.event_eq(event); //사용 가능하게 설정된 업로더에서의 인덱스
                    let loaderMain   = document.querySelectorAll(obj_code.loader_main_cls)[eq_idx];
                    let preview_mode = String(obj_code.preview_mode[eq_idx]);    // 파일 미리 보기 창 열려 있는지에 대한 플래그
                    let detail_mode  = String(obj_code.detail_mode[eq_idx]);     // 파일 상세 보기 창 열려 있는지에 대한 플래그
                    let openMode     = (preview_mode === 'open' || detail_mode === 'open' ) ? 'open' : 'close';// 미리 보기,상세 보기 창을 열었는지
                    let fullView     = loaderMain.querySelector('.file_zone_full_view');
                    let viewDp       = loaderMain.querySelector('.file_view_dp');
                    let fullIcon     = fullView.querySelector('i');
                    let previewDom   = loaderMain.querySelector('.view_preview');
                    let detailDom    = loaderMain.querySelector('.view_detail');
 
                    if (loaderMain.parentNode.classList.contains('full')) { // 전체 화면, 문서 최대크기
                        let loaderWidth1 = '';
                        let viewerWidth1 = '';
                        if( openMode === 'open' ){
                            loaderWidth1 = loaderMain.offsetWidth;
                            if(preview_mode === 'open'){
                                viewerWidth1 = previewDom.offsetWidth;
                            } 
                            else 
                            if(detail_mode === 'open'){ 
                                viewerWidth1 = detailDom.offsetWidth;
                            }
                        }
   
                        if(document.fullscreenElement){
                            if(obj_code.exit_screen === 'full_window'){ // 전체 화면에서 문서 최대 크기로
                                obj_util.normall_screen();
                                MARI.file_uploder.code.screen_mode[eq_idx] = 'full';

                                // 미리보기, 상세보기창이 열려 있다면 리사이즈
                                if( openMode === 'open' ){
                                    let loaderWidth2 = loaderMain.offsetWidth;  
                                    if(preview_mode === 'open'){
                                        viewerWidth2 = previewDom.offsetWidth;
                                    } 
                                    else 
                                    if(detail_mode === 'open'){ 
                                        viewerWidth2 = detailDom.offsetWidth;
                                    }
                                    let a = loaderWidth1;
                                    let b = loaderWidth2;
                                    let c = viewerWidth1;
                                    let x = (b * c) / a;  // 재 계산되어야 할 넓이

                                    previewDom.style.width = x + 'px';
                                    detailDom.style.width  = x + 'px'; 
                                    MARI.file_uploder.code.view_dp_wd[eq_idx] =  Math.round(x); // Math.round(x * 10) / 10
                                } 
                                return true;
                            }
                        }
                        // 문서 최대크기에서 일반 크기로
                        obj_util.normall_screen();
                        obj_code.screen_mode[eq_idx] = 'normal';
                        loaderMain.parentNode.classList.remove('full');
                        fullIcon.classList.replace('normal_screen', 'full_screen');
                        document.documentElement.style.overflowY = "auto";
                        viewDp.style.width =  Math.min(obj_code.preview_wd, obj_code.max_preview_wd) + 'px';

                        // 미리보기, 상세보기창 리사이즈  
                        if( openMode === 'open' ){
                            let loaderWidth3 = loaderMain.offsetWidth;  
                            if(preview_mode === 'open'){
                                viewerWidth3 = previewDom.offsetWidth;
                            } 
                            else 
                            if(detail_mode === 'open'){ 
                                viewerWidth3 = detailDom.offsetWidth;
                            }
                            let a = loaderWidth1;
                            let b = loaderWidth3;
                            let c = viewerWidth1; 
                            let x = (b * c) / a;  // 재 계산되어야 할 넓이 
                            if(x < parseInt(obj_code.min_preview_wd)){ // 최소한의 크기를 유지한다
                                x = parseInt(obj_code.min_preview_wd);
                            }
                            
                            MARI.file_uploder.code.view_dp_wd[eq_idx] =   Math.round(x); // Math.round(x * 10) / 10
                            previewDom.style.width = x + 'px';
                            detailDom.style.width  = x + 'px';

                            // 일반 크기로
                            obj_code.setPositionImg(eq_idx, 'first');
                        }

                    } else { // 일반 크기

                        let loaderWidth4 = ''; 
                        let viewerWidth4 = ''; 
                        // 미리보기, 상세보기창 리사이즈  
                        if( openMode === 'open' ){
                            loaderWidth4 = loaderMain.offsetWidth;  
                            if(preview_mode === 'open'){
                                viewerWidth4 = previewDom.offsetWidth;
                            } 
                            else 
                            if(detail_mode === 'open'){ 
                                viewerWidth4 = detailDom.offsetWidth;
                            }
                        }
                        // 문서 최대 크기로
                        loaderMain.parentNode.classList.add('full');
                        fullIcon.classList.replace('full_screen', 'normal_screen');
                        document.documentElement.style.overflowY = "hidden";
                        // 미리보기, 상세보기창 리사이즈

                        // 전체 화면 크기로
                        if (obj_code.use_full_screen === 'Yes') {
                            obj_code.screen_mode[eq_idx] = 'full_window';
                            loaderMain.requestFullscreen();
                        }

                        obj_code.screen_mode[eq_idx] = 'full';
                        // 미리보기, 상세보기창 리사이즈   
                        if( openMode === 'open' ){
                            let loaderWidth5 = loaderMain.offsetWidth;
                            if(preview_mode === 'open'){
                                viewerWidth5 = previewDom.offsetWidth;
                            } 
                            else 
                            if(detail_mode === 'open'){ 
                                viewerWidth5 = detailDom.offsetWidth;
                            }
                            let a = loaderWidth4;
                            let b = loaderWidth5;
                            let c = viewerWidth4;
                            let x = (b * c) / a;  // 재 계산되어야 할 넓이 

                            previewDom.style.width = x + 'px';
                            detailDom.style.width  = x + 'px'; 
                            MARI.file_uploder.code.view_dp_wd[eq_idx] =  Math.round(x); // Math.round(x * 10) / 10

                            //최대 크기로
                            obj_code.setPositionImg(eq_idx, 'first');
                        }
                    }

                    /*===========================================
                    ** 파일의 위치 정보
                    ** 파일 미리보기 안에 들어 있는 파일들의 위치 정보를 기록한다
                    ** 마우스 다운시 마다 정보가 입력되니 파일 추가등 변황 있을때 마다 
                    ** 넣을수 있도록	 변화 시킬 것
                    ** 파일 추가 삭제시 때만 들어가도록 넣을 수 있음
                    **-----------------------------------------*/
                    obj_code.position_info();  

                    // 미리보기 드래그바 보이기 또는 감추기 활성 여부
                    obj_code.drag_bar_line_dp(event, eq_idx);
                }
            });

        }, //end full_window


        /**
         * 주어진 너비와 높이를 최대 크기(maxSize) 내에서 비율을 유지하며 리사이징
         *
         * @param {number} width   - 원본 너비
         * @param {number} height  - 원본 높이
         * @param {number} maxSize - 최대 크기 (너비 또는 높이 중 가장 큰 값이 이 크기를 넘지 않도록 조정)
         * @returns {{width: number, height: number}} 리사이징된 너비와 높이
         */
        resizeToFit : function (width, height, maxSize) {
            let scale = maxSize / Math.max(width, height);
            
            return {
                width: Math.round(width * scale),
                height: Math.round(height * scale)
            };

        },


		/*=========================================================================================
		 * 0. 파일 관련
		**---------------------------------------------------------------------------------------*/
        /**
         * 파일명을 통한 확장자 가져오기
         * getFileExtension("script.js")       => script.js
         * getFileExtension("script.js", true) => script.JS
        */
        getFileExtension: function (filename, toUpperCase = false){
            let ext = filename.includes('.') ? filename.split('.').pop() : '';

            return toUpperCase ? ext.toUpperCase() : ext.toLowerCase();
        },
            
        /**
         * 파일명을 통한 이미지 파일인지에 대한 여부
         *  isImageFile("photo.png");     // true
         *  isImageFile("document.pdf")); // false
        */
        isImageFile : function (filename) {
            var imageExtensions = new Set([
                // 일반 이미지 포맷
                'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff', 'tif', 'ico', 
                'jfif', 'pjpeg', 'pjp', 'avif', 'heic', 'heif',

                // RAW 이미지 포맷 (DSLR/미러리스 카메라)
                'raw', 'cr2', 'cr3', 'nef', 'nrw', 'orf', 'sr2', 'srf', 'arw', 'rw2', 'rwl', 'pef', 'dng', 'raf', 'k25', 'kdc',

                // 그래픽 및 디자인 관련 포맷
                'psd', 'ai', 'eps', 'indd', 'qxp', 'cdr', 'cpt', 'sketch', 'xcf', 

                // 3D 이미지 및 특수 포맷
                'exr', 'hdr', 'dds', 'tga', 'sgi', 'rgba', 'bw', 'jxr',

                // 애니메이션/프레임 기반 이미지 포맷
                'apng', 'flif', 'mng'
            ]);
            return imageExtensions.has(filename.split('.').pop().toLowerCase());
        },
 
 
        /**
         * 파일명 재 생성
        */
        generateUniqueFileName: function (fileName, existingNames, objType = 'New') {
            // 파일 확장자 추출
            let fileExtension = fileName.match(/\.[^/.]+$/);                         // 파일 확장자 (.jpg 등)
            let baseName = fileName.replace(/\.[^/.]+$/, '');                        // 파일 이름

            // objType이 'copy'일 때 복사본 처리
            let newFileName = (objType === 'copy') ? baseName + ' - 복사본' : baseName;  // '파일명-복사본'

            let count = 1; // 숫자는 1부터 시작
            let fullFileName = newFileName + (fileExtension || '');

            // 중복된 파일명을 찾을 때까지 숫자를 붙임
            while (existingNames.includes(fullFileName)) {
                count++;
                fullFileName = newFileName + `(${count})` + (fileExtension || '');
            }

            return fullFileName; // 중복이 없는 고유 파일명 반환
        },



        ///////// 썸네일 생성
        async generateFileWithThumbnail(url, maxSize = 150) {
            const file = await this.urlToFile(url);
            const thumbnail = await this.createThumbnail(file, maxSize);
            return { file, thumbnail };
        },

        async urlToFile(url, fileName = "image.jpg") {
            const response = await fetch(url);
            const blob = await response.blob();
            return new File([blob], fileName, { type: blob.type });
        },

        async createThumbnail(file, maxSize = 150) {
            return new Promise((resolve) => {
                const img = new Image();
                img.src = URL.createObjectURL(file);
                img.onload = () => {
                    const canvas = document.createElement("canvas");
                    let { width, height } = img;

                    // 비율 유지하면서 리사이징
                    if (width > height) {
                        height *= maxSize / width;
                        width = maxSize;
                    } else {
                        width *= maxSize / height;
                        height = maxSize;
                    }

                    canvas.width  = width;
                    canvas.height = height;
                    const ctx = canvas.getContext("2d");
                    ctx.drawImage(img, 0, 0, width, height);

                    // Canvas → Blob 변환 (썸네일)
                    canvas.toBlob((blob) => {
                        const thumbFile = new File([blob], "thumbnail.jpg", { type: "image/jpeg" });
                        resolve(thumbFile);
                    }, "image/jpeg", 0.9);
                };
            });
        },
        ///////// 썸네일 생성





        /**
         * UTF-8 추가 검사 함수
        */
        checkUTF8 : function(buffer) {
            let i = 0;
            while (i < buffer.length) {
                if (buffer[i] <= 0x7F) {
                    i += 1;
                } else if ((buffer[i] & 0xE0) === 0xC0) {
                    if (i + 1 < buffer.length &&
                        (buffer[i + 1] & 0xC0) === 0x80) {
                        i += 2;
                    } else {
                        return false;
                    }
                } else if ((buffer[i] & 0xF0) === 0xE0) {
                    if (i + 2 < buffer.length &&
                        (buffer[i + 1] & 0xC0) === 0x80 &&
                        (buffer[i + 2] & 0xC0) === 0x80) {
                        i += 3;
                    } else {
                        return false;
                    }
                } else if ((buffer[i] & 0xF8) === 0xF0) {
                    if (i + 3 < buffer.length &&
                        (buffer[i + 1] & 0xC0) === 0x80 &&
                        (buffer[i + 2] & 0xC0) === 0x80 &&
                        (buffer[i + 3] & 0xC0) === 0x80) {
                        i += 4;
                    } else {
                        return false;
                    }
                } else {
                    return false;
                }
            }

            return true;

        },


        /**
         * Base64 데이터 URL에서 MIME 타입 추출
        */ 
		extractMimeType : function (base64DataUrl) {
			const matches = base64DataUrl.match(/^data:(.*?);base64,/);

			return matches ? matches[1] : null;

		},


        /**
         * base64 문자열을 Blob으로 변환하는 함수
        */
		base64ToBlob : function (base64, mimeType) {
			const byteCharacters = atob(base64);
			const byteNumbers = new Array(byteCharacters.length);
			for (let i = 0; i < byteCharacters.length; i++) {
				byteNumbers[i] = byteCharacters.charCodeAt(i);
			}
			const byteArray = new Uint8Array(byteNumbers);

			return new Blob([byteArray], { type: mimeType });

		},


        /**
         * APIC (Album Art) ID3 프레임에서 이미지 데이터를 추출
        */
        extractImageData:function (apicFrame) {
            /*===================================
             * APIC (Album Art) ID3 프레임에서 이미지 데이터를 추출하는 함수
             * apicFrame => APIC 프레임을 나타내는 배열 (ID3v2 태그 내에 포함된 이미지 데이터)
             *
             * 과정:
             * - 텍스트 인코딩을 추출 (ID3v2에서 사용되는 텍스트 인코딩 방식)
             * - MIME 타입 (예: image/jpeg, image/png)을 추출
             * - 이미지 타입을 추출 (예: 그림, 아이콘 등)
             * - 설명 부분은 추출하고, 마지막으로 실제 이미지 데이터를 추출하여 반환
             *
             * 반환값:
             * - mimeType: 이미지의 MIME 타입 (예: 'image/jpeg')
             * - imageData: 이미지 데이터 (바이트 배열)
             *
             * 사용 예시:
             * const frame = [ ... ]; // APIC 프레임 데이터
             * const { mimeType, imageData } = extractImageData(frame);
             *---------------------------------*/
            let offset = 0;
            const encoding = apicFrame[offset]; // 텍스트 인코딩
            offset += 1;

            const mimeTypeEnd = apicFrame.indexOf(0x00, offset);
            const mimeType = String.fromCharCode(...apicFrame.slice(offset, mimeTypeEnd));
            offset = mimeTypeEnd + 1;

            const pictureType = apicFrame[offset];
            offset += 1;

            const descriptionEnd = apicFrame.indexOf(0x00, offset);
            offset = descriptionEnd + 1;

            const imageData = apicFrame.slice(offset);

            return { mimeType, imageData };

        },


        /**
         * ID3v2 태그의 전체 크기를 계산하는 함수
        */
        getID3v2Size : function (data) {
            /*===================================
             * ID3v2 태그의 전체 크기를 계산하는 함수
             * data => ID3v2 태그를 포함한 바이너리 데이터 배열
             *
             * 과정:
             * - ID3v2 태그의 시작을 확인 ('ID3' 문자열이 첫 3바이트에 존재)
             * - ID3v2 태그의 크기를 나타내는 4바이트(크기 정보)를 읽어와서 실제 크기를 계산
             * - 계산된 크기에 헤더 크기(10바이트)를 더하여 전체 태그 크기를 반환
             *
             * 반환값:
             * - ID3v2 태그의 전체 크기 (헤더 크기 포함)
             * - 'ID3' 태그가 없는 경우 0 반환
             *
             * 사용 예시:
             * const data = [ ... ]; // ID3v2 태그 데이터
             * const size = getID3v2Size(data); // 전체 크기 계산
             *---------------------------------*/
            if (String.fromCharCode(...data.slice(0, 3)) === 'ID3') {
                const sizeBytes = data.slice(6, 10);
                const size = (sizeBytes[0] & 0x7F) * 0x200000 +
                             (sizeBytes[1] & 0x7F) * 0x4000 +
                             (sizeBytes[2] & 0x7F) * 0x80 +
                             (sizeBytes[3] & 0x7F);
                return size + 10; // Header size (10 bytes) 포함
            }

            return 0;

        },


        /**
         * ID3v2 태그에서 APIC (앨범 아트 이미지) 프레임을 찾아서 반환하는 함수
        */
        findAPICFrame :function (data, id3HeaderSize) {
            /*===================================
             * ID3v2 태그에서 APIC (앨범 아트 이미지) 프레임을 찾아서 반환하는 함수
             * data => ID3v2 태그를 포함한 바이너리 데이터 배열
             * id3HeaderSize => ID3v2 태그의 전체 크기 (헤더 크기 포함)
             *
             * 과정:
             * - ID3v2 태그의 헤더 크기를 넘기고, 각 프레임을 순차적으로 탐색
             * - 'APIC' 프레임을 찾으면 해당 프레임의 데이터를 반환
             * - 'APIC' 프레임이 없으면 null 반환
             *
             * 반환값:
             * - 'APIC' 프레임이 있을 경우 해당 프레임 데이터 (앨범 아트 이미지 데이터)
             * - 'APIC' 프레임이 없으면 null
             *
             * 사용 예시:
             * const data = [ ... ]; // ID3v2 태그 데이터
             * const apicData = findAPICFrame(data, id3HeaderSize); // APIC 프레임 데이터 추출
             *---------------------------------*/
            let offset = 10; // ID3 v2 헤더를 넘기기 위한 초기 오프셋
            while (offset < id3HeaderSize) {
                const frameID = String.fromCharCode(...data.slice(offset, offset + 4));
                const frameSize = (data[offset + 4] << 24) |
                                  (data[offset + 5] << 16) |
                                  (data[offset + 6] << 8) |
                                   data[offset + 7];

                if (frameID === 'APIC') {
                    return data.slice(offset + 10, offset + 10 + frameSize);
                }

                offset += 10 + frameSize; // 다음 프레임으로 이동 (헤더 크기 10바이트)
            }
            return null;
        },
            
        
        /**
         * ID3v2 태그에서 APIC (앨범 아트 이미지) 프레임을 찾아서 반환하는 함수
        */ 
        arrayBufferToBase64 : function (buffer) {
            /*===================================
             * ArrayBuffer를 Base64 문자열로 변환하는 함수
             * buffer => ArrayBuffer 객체 (바이너리 데이터)
             *
             * 과정:
             * - ArrayBuffer의 각 바이트를 문자로 변환하여 문자열로 연결
             * - 그 문자열을 Base64로 인코딩
             *
             * 반환값:
             * - 변환된 Base64 인코딩된 문자열
             *
             * 사용 예시:
             * const buffer = new ArrayBuffer(10); // 예시로 10바이트 크기 배열 버퍼
             * const base64Str = arrayBufferToBase64(buffer); // ArrayBuffer를 Base64로 변환
             *---------------------------------*/
            let binary = '';
            const bytes = new Uint8Array(buffer);
            for (let i = 0; i < bytes.byteLength; i++) {
                binary += String.fromCharCode(bytes[i]);
            }
            return window.btoa(binary);
        },
 
 
		/*=========================================================================================
		 * 0. 오디오 관련
		**---------------------------------------------------------------------------------------*/

        /**
         * 미리보기에서  mp3선택시 자동으로 플레이
        */ 
        audioAutoPlay : function (event, eq_idx) {

            /*===================================
            ** 오디오 형식의 미리보기라면 오디오 자동 출력 때 사용
            **---------------------------------*/
            let obj_util = MARI.file_uploder.util;
            let obj_code = MARI.file_uploder.code; 
            let loaderMain = obj_code.loader_main_obj(eq_idx); 
            if (!loaderMain || obj_code.preview_mode[eq_idx] === 'close') return false; // 미리보기 창이 닫혀 있다면 플레이하지 않음

            let audioPlayer = loaderMain.querySelector('.audio_player');  // 선택된 업로더의 오디오 플레이어 

            if (obj_code.drag_flag === false) { // 드래그와 선택 중이 아니라면
                // ※ opt_check4 명칭이 맞아야 한다 오토 플레이 안되면 확인할 것
                const checkbox = loaderMain.querySelector('.opt_check4');
                if (checkbox && checkbox.checked && audioPlayer !== null) {
                    let audioTag = audioPlayer.querySelector('source');
                    let audioSrc = audioTag ? audioTag.src : null;
                    let contType = null;

                    async function isAudioSource(src) {
                        try {
                            let response = await fetch(src);
                            let contentType = response.headers.get('Content-Type');
                            contType = contentType;
                            return contentType && contentType.startsWith('audio/');
                        } catch (error) {
                            console.error('Error fetching the resource:', error);
                            return false;
                        }
                    }

                    isAudioSource(audioSrc).then(isAudio => {
                        if (isAudio) { // 오디오 형식이라면

                            // 현재 페이지 내에 플레이 중인 모든 오디오 정지
                            document.querySelectorAll('.audio_player').forEach(allPlayer => {
                                
                                if (allPlayer instanceof HTMLAudioElement) {
                                    try {
                                        allPlayer.pause(); 
                                        allPlayer.currentTime = 0; 
                                    } catch (error) {
                                        console.error('Audio stop failed:', error);
                                    }
                                }
                            });
            
                            // 선택한 오디오 플레이어 재생
                            let nowPlayer = loaderMain.querySelector('.audio_player');
                            if (nowPlayer instanceof HTMLAudioElement) {
                                nowPlayer.volume = 0.1;   // 최소 볼륨 설정
                                nowPlayer.muted  = false; 
            
                                // play() 호출과 관련된 오류를 안전하게 처리
                                nowPlayer.play().then(() => {
                                    //console.log('오디오가 성공적으로 재생되었습니다.');
                                }).catch(error => {
                                    if (error.name === 'AbortError') {
                                    // console.warn('재생이 중단되었습니다. 오디오가 DOM에서 삭제되었을 수 있습니다.');
                                    } else {
                                    // console.error('재생 중 오류 발생:', error);
                                    }
                                });

                                // 기존 볼륨 증가 인터벌이 있는 경우 삭제
                                if (obj_util.volumeInterval) clearInterval(obj_util.volumeInterval);

                                // 볼륨 점진적 증가
                                obj_util.volumeInterval = setInterval(() => {
                                    if (nowPlayer.volume < 0.8) {
                                        nowPlayer.volume = Math.min(nowPlayer.volume + 0.01, 0.8);
                                    } else {
                                        clearInterval(obj_util.volumeInterval);
                                    }
                                }, 100);
                            }// 선택한 오디오 플레이어 재생
 
                        } else { 
                            alert('오디오 파일이 아닙니다 :: ' + contType);
                        }

                    });
                } else {//checkbox
                //    console.log('체크박스가 체크되어 있지 않습니다.');
                }
            }//// 드래그와 선택 중이 아니라면

        },
        volumeInterval: null,


		/*=========================================================================================
		 * 0. 효과 관련
		**---------------------------------------------------------------------------------------*/
        
        /**
         * 페이드 인
        */
        fadeIn : function(element, duration, delay = 0, dp = 'block', targetOpacity = 1, interval = 10, code = () => {}) {
            /*===================================
             * 제이쿼리의 페이드 인 효과
             * element        => 애니메이션을 적용할 HTML 요소
             * duration       => 애니메이션 지속 시간 (밀리초 단위)
             * delay          => 애니메이션 시작 전 지연 시간 (밀리초 단위)
             * dp             => 요소의 display 속성 값 ('block' 등) :: 상황에 따라 요소가 보이도록 설정할 때 사용
             * targetOpacity  => 애니메이션 종료 시 투명도 값 (기본값은 1로 완전 불투명)
             * interval       => 애니메이션 업데이트 간격 (밀리초 단위, 기본값 10ms)
             * code           => 애니메이션이 끝난 후 실행할 콜백 함수 (기본값은 빈 함수)
             *
             * 사용 예시:
             * let fileEditor = document.querySelector('#fileEditor'); // fileEditor 요소 선택
             * fadeIn(fileEditor, 500, 10); -> 10ms 지연 후 500ms 동안 서서히 나타남
             * fadeIn(fileEditor, 500);     -> 지연 없이 500ms 동안 서서히 나타남
             * fadeIn(fileEditor, 500, 150, 'block', 1, 10, function() { alert(); }); 
             * -> 150ms 지연 후 500ms 동안 투명도가 1이 될 때까지 서서히 나타나고, 끝난 후 alert() 실행
             * fadeIn(fileEditor, 500, 10, ''); -> 빈 문자열로 display 설정 -> 기본 CSS 값에 따라 display 결정
             *---------------------------------*/
            if(!element) return false;

            // 중복 호출 방지
            if (element.isFading) return;  
            element.isFading = true;     // 애니메이션 진행 중 표시
    
            element.style.opacity = 0;   // 시작시 투명하게 설정
            if( dp == 'block')
            element.style.display = dp;  // 요소를 화면에 표시 :: 

            let opacity = 0;

            //interval 권장 50 50ms 간격으로 실행 ( 인간의 눈은 매 16.67밀리초(즉, 60프레임/초)보다 빠른 프레임을 인식하기 어렵다 )
            const increment = interval / duration; // 증가할 opacity 값 계산
            // 딜레이 설정
            setTimeout(function() {
                const fading = setInterval(function() {
                    opacity += increment; // opacity 값을 점진적으로 증가
                    element.style.opacity = opacity;
                    if (opacity >= targetOpacity) { // opacity가 설정값에 도달하면 중지
                        clearInterval(fading);
                        element.style.opacity = targetOpacity;
                        element.isFading = false;  // 애니메이션 종료 후 해제
                        // code(); // 매개변수로 받은 함수를 실행 
                        // 비동기적으로 실행되도록 setTimeout 사용 :: 페이드인이 끝난 후에 동작되도록
                        setTimeout(code, (duration + delay) );  
                    }
                }, interval);
            }, delay); // 지연 시간

        },


        /**
         * 페이드 아웃
        */
        fadeOut : function(element, duration, delay = 0, dp = 'none', targetOpacity = 0, interval = 10, code = () => {}) {
            /*===================================
             * 제이쿼리의 페이드 아웃 효과
             * element        => 애니메이션을 적용할 HTML 요소
             * duration       => 애니메이션 지속 시간 (밀리초 단위)
             * delay          => 애니메이션 시작 전 지연 시간 (밀리초 단위)
             * dp             => 요소의 display 속성 값 ('none' 등) :: 요소를 숨기도록 설정할 때 사용
             * targetOpacity  => 애니메이션 종료 시 투명도 값 (기본값은 0으로 완전 투명)
             * interval       => 애니메이션 업데이트 간격 (밀리초 단위, 기본값 10ms)
             * code           => 애니메이션이 끝난 후 실행할 콜백 함수 (기본값은 빈 함수)
             *
             * 사용 예시:
             * let fileEditor = document.querySelector('#fileEditor'); // fileEditor 요소 선택
             * fadeOut(fileEditor, 500, 10); -> 10ms 지연 후 500ms 동안 서서히 사라짐
             * fadeOut(fileEditor, 500);     -> 지연 없이 500ms 동안 서서히 사라짐
             * fadeOut(fileEditor, 500, 150, 'block', 1, 10, function() { alert(); }); 
             * -> 150ms 지연 후 500ms 동안 투명도가 0이 될 때까지 서서히 사라지고, 끝난 후 alert() 실행
             *---------------------------------*/
            if(!element) return false;

            // 중복 호출 방지
            if (element.isFading) return;  
            element.isFading = true;       // 애니메이션 진행 중 표시
            
            element.style.opacity = 1; // 시작시 완전히 보이게 설정

            let opacity = 1;

            //interval 권장 50 50ms 간격으로 실행 ( 인간의 눈은 매 16.67밀리초(즉, 60프레임/초)보다 빠른 프레임을 인식하기 어렵다 )
            const decrement = interval / duration; // 감소할 opacity 값 계산

            // 딜레이 설정
            setTimeout(function() {
                const fading = setInterval(function() {
                    opacity -= decrement; // opacity 값을 점진적으로 감소
                    element.style.opacity = opacity; 
                    if (opacity <= targetOpacity) { // opacity가 0에 도달하면 중지
                        clearInterval(fading);
                        element.style.display = dp;
                        element.style.opacity = targetOpacity;// opacity가 설정값에 도달하면 중지

                        element.isFading = false;  // 애니메이션 종료 후 해제
                        //code(); // 매개변수로 받은 함수를 실행
                        // 비동기적으로 실행되도록 setTimeout 사용 :: 페이드인이 끝난 후에 동작되도록
                        setTimeout(code, (duration + delay) );  
                    }
                }, interval); 
            }, delay); // 지연 시간  

        },


		/*=========================================================================================
		 * 0. 문자 관련
		**---------------------------------------------------------------------------------------*/
        
        /**
         * 좌우 공백 제거
        */
        trim : function(str, objname){ 
            if(str === null){
                alert(objname+'의 입력값 ( '+ str +' ) 문자열이 null상태입니다');
                return false;
            }
            else{
                return str.replace(/^\s+|\s+$/g,""); 
            }

        },


        /**
         * 우측 공백 제거
        */
        rtrim : function(str, objname){
            if(str === null){
                alert(objname+'의 입력값 ( '+ str +' ) 문자열이 null상태입니다');
                return false;
            }
            else{
                return str.replace(/\s+$/,""); 
            }

        },


        /**
         * 왼쪽 공백 제거
        */ 
        ltrim : function(str, objname){
            if(str === null){
                alert(objname+'의 입력값 ( '+ str +' ) 문자열이 null상태입니다');
                return false;
            }
            else{
                return str.replace(/^\s+/,"");
            }

        },


		/*=========================================================================================
		 * 0. 배열 관련
		**---------------------------------------------------------------------------------------*/
        
        /**
         *  오름 근사값::인덱스
        */
        findNextHigherIndex : function(arr, target) {  
            /*===================================
             * 주어진 배열에서 타겟 값보다 큰 첫 번째 요소의 인덱스를 찾는 함수
             * arr      => 숫자 배열
             * target   => 비교할 타겟 값
             *
             * 과정:
             * - 배열을 순차적으로 탐색하여 target 값보다 큰 요소가 있는지 확인
             * - findIndex() 메서드를 사용하여 타겟 값보다 큰 첫 번째 요소의 인덱스를 반환
             *
             * 반환값:
             * - 타겟 값보다 큰 첫 번째 요소의 인덱스 (없으면 -1 반환)
             *
             * 사용 예시:
             * const arr = [1, 3, 5, 7, 9];
             * const index = findNextHigherIndex(arr, 4);  // 2 반환 (5가 target(4)보다 크므로)
             * const index2 = findNextHigherIndex(arr, 10); // -1 반환 (10보다 큰 값이 없으므로)
             *---------------------------------*/
            return arr.findIndex(value => value > target); 
            
        },
            

        /**
         * 내림 근사값::인덱스
        */
        findBeforeHigherIndex : function(arr, target) { 
            return arr.findIndex(value => value < target); 
        },


		/*=========================================================================================
		 * 0. 기타
		**---------------------------------------------------------------------------------------*/

        /**
         * 간단하게 개체를 마우스로 이동시키는 스크립트 
        */
        dragElement : function (container, handle) {
          var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
          if(handle){
          handle.onmousedown = dragMouseDown;
          function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault(); 
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement; 
            document.onmousemove = elementDrag;
          }
          function elementDrag(e) {
            e = e || window.event;
            e.preventDefault(); 
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY; 
            container.style.top  = (container.offsetTop - pos2) + "px";
            container.style.left = (container.offsetLeft - pos1) + "px";
          }
          function closeDragElement() { 
            document.onmouseup = null;
            document.onmousemove = null;
          }
          }
        },

 
		/*=========================================================================================
		 * 0. 뷰어 관련
		**---------------------------------------------------------------------------------------*/
        
        /**
         * zip 압축 파일 내용보기
        */ 
        parseZIP : function (data) {
           // const output = document.getElementById("output");
           // output.innerHTML = ""; // 기존 내용 삭제

            let outputMsg = '';
            let pos = data.length - 22; // ZIP 파일의 끝 부분을 찾아야 함 (End of Central Directory)
            while (pos >= 0) {
                if (data[pos] === 0x50 && data[pos + 1] === 0x4B && data[pos + 2] === 0x05 && data[pos + 3] === 0x06) {
                    break; // "PK\x05\x06" → ZIP 파일의 끝을 의미
                }
                pos--;
            }

            if (pos < 0) {
                outputMsg = "유효한 ZIP 파일이 아닙니다.";
                return;
            }

            outputMsg = "<strong>ZIP 파일 내부 목록:</strong><br>";
            for (let i = 0; i < data.length; i++) {
                if (data[i] === 0x50 && data[i + 1] === 0x4B && data[i + 2] === 0x03 && data[i + 3] === 0x04) {
                    let nameLength = data[i + 26] | (data[i + 27] << 8);
                    let name = new TextDecoder().decode(data.slice(i + 30, i + 30 + nameLength));
                    outputMsg += `📄 ${name}<br>`;
                }
            }
            
            return outputMsg;
        }

	};// end property 
}
