PageSourceSearch

https://cdn.ccreview.co.kr/script/Util.js

js ccreview.co.kr collected 2026-10-02 19:09:52 UTC 33,636 bytes, 818 lines download raw bytes

1/**
2 * 여러 곳에 사용할 메서드
3 */
4class Util {
5    static animationState = {};
6    static fileExt = {  img:["jpeg","jpg","png","gif"], ico:["ico"], file:["zip", "alz", "xls", "doc", "ppt", "xlsx", "docx", "pptx", "hwp", "pdf", "txt"] };
7
8    // 모바일 구분
9    static isMobileDevice(){
10		return /(iPhone|iPod|IPad|Android|Blackberry|Opera Mini|Windows CE|Nokia|Sony|Samsung|LGTelecom|SKT|Mobile|Phone)/.test(navigator.userAgent);
11	}
12
13    // 인코딩 데이터를 디코딩하고 JSON 객체로 파싱.
14    static decodeJson(encodedData) {
15        const decodedData = decodeURIComponent(encodedData);
16        return JSON.parse(decodedData);
17    }
18
19    //rawurldecode
20	static rawurldecode(str) {
21		return decodeURIComponent((str + '')
22			.replace(/%(?![\da-f]{2})/gi, function() {
23				return '%25';
24			}));
25	}
26
27    // HTML Entity를 해당 문자로 변환
28    static unescapeHtml(html) {
29        let txt = document.createElement("textarea");
30        txt.innerHTML = html;
31        return txt.value;
32    }
33
34    // 파일 허용 확장자 체크
35    static isAllowFileExt(filename, filetype="img"){
36        if(!filename || !filetype) return false;
37
38        const filenames = filename.split(".")
39            , ext = filenames.pop().toLowerCase()
40            , allow = this.fileExt[filetype];
41        
42        if(filenames.length<=0) return false;
43        if(allow.includes(ext)) return true;
44
45        return false;
46    }
47
48	// www를 제외한 도메인 (쿠키 구울시 전체적으로 적용을 위함 ex. .domain.com, 서브 도메인 형식은 치환이 안되서 OR 조건 추가)
49	static getDomainName(domain=SITE_DOMAIN){
50		return domain.replace(/^http(s?):\/\/|www\./g,'');
51	}
52
53    // date, time, picker 등  input 영역 클릭해도 picker 노출되게 이벤트 추가
54    static showPickerOnInput(){
55        const inputs = document.querySelectorAll("input[type='date'],input[type='time'],input[type='datetime-local'],input[type='month'],input[type='week']")
56        for(const input of inputs){
57            input.addEventListener("click", e=>e.target.showPicker());
58        }        
59    }
60
61    // 왼쪽 메뉴 펴치기
62    static slideLeftMenuBox(){
63        const   wrap = document.querySelector("#wrap"),
64                gridSide = wrap.querySelector("#grid-side");
65
66        gridSide.addEventListener("mouseenter", e=>{
67            if(!wrap.classList.contains('is-active')) wrap.classList.add("is-hover");
68        });
69        
70        gridSide.addEventListener("mouseleave", e=>{
71            if(!wrap.classList.contains('is-active')) wrap.classList.remove("is-hover");
72        });
73    }
74
75    /**
76     * 텍스트 복사 기능
77     * @param {string} source       - 복사하고자 하는 텍스트.
78     * @param {function} onSuccess  - 복사가 성공했을 때 호출될 콜백 함수. 
79                                    - 기본값은 사용자에게 "복사되었습니다. 원하는 곳에 붙여넣기하여 주세요." 알림 창 표시
80     * @param {function} onFailure  - 복사가 실패했을 때 호출될 콜백 함수.
81                                    - 기본값은 사용자에게 "키보드의 ctrl+C 또는 마우스 오른쪽의 복사하기를 이용해주세요."라는 메시지와 함께 복사하려는 텍스트를 제공하는 프롬프트 창을 표시
82     */                          
83    static copyToClipboard(source, onSuccess, onFailure) {
84        const defaultSuccess = () => alert("복사되었습니다. 원하는 곳에 붙여넣기하여 주세요.");
85        const defaultFailure = () => prompt("키보드의 ctrl+C 또는 마우스 오른쪽의 복사하기를 이용해주세요.", source);
86        if (navigator.clipboard) {
87            navigator.clipboard.writeText(source).then(onSuccess || defaultSuccess).catch(onFailure || defaultFailure);
88        } else {
89            const textarea = document.createElement("textarea");
90            document.body.appendChild(textarea);
91            textarea.value = source;
92            textarea.select();
93            try {
94                if (document.execCommand("copy")) {
95                    (onSuccess || defaultSuccess)();
96                } else {
97                    (onFailure || defaultFailure)();
98                }
99            } catch (err) {
100                (onFailure || defaultFailure)();
101            } finally {
102                document.body.removeChild(textarea);
103            }
104        }
105    }
106    
107    // copyToClipboard 실행방식이 다 비숫해서 만듦
108    static copyExec(selector, onSuccess = null, onFailure = null) {
109        const buttons = document.querySelectorAll(selector);
110        for (const btn of buttons) {
111            btn.addEventListener("click", e => {
112                const target = e.target.closest(selector);
113                let text = '';
114    
115                if (target && target.getAttribute("data-clipboard-text")) {
116                    text = target.getAttribute("data-clipboard-text");
117                }
118                else if (target && target.getAttribute("data-clipboard-target")) {
119                    const targetSelector = target.getAttribute("data-clipboard-target");
120                    const targetElement = document.querySelector(target
120Selector);
121                    text = targetElement ? (targetElement.value || targetElement.textContent) : '';
122                }
123    
124                if (text) {
125                    this.copyToClipboard(text,
126                        onSuccess ? () => onSuccess(text, target) : null,
127                        onFailure ? () => onFailure(text, target) : null
128                    );
129                }
130            });
131        }
132    }
133    
134    
135    static createClipboard(target=".clipboard-view") {
136        let timeoutId;
137
138        const clipboard = document.createElement("div");
139        clipboard.className = "clipboard";
140        const clipbdToast = document.createElement("div");
141        clipbdToast.id = "clipbd-toast";
142        clipbdToast.className = "toast"
143        const clipbdToastBody = document.createElement("div");
144        clipbdToastBody.className = "toast-body";
145        const btnClose = document.createElement("button");
146        btnClose.type = "button";
147        btnClose.className = "btn-close";
148        btnClose.onclick=e=>{ clipboard.classList.remove("active"); };
149
150        clipbdToast.appendChild(clipbdToastBody);
151        clipbdToast.appendChild(btnClose);
152        clipboard.appendChild(clipbdToast);
153
154        const clipboardView = document.querySelectorAll(target);
155
156        clipboardView.forEach(element => {
157            element.addEventListener("click", (e) => {
158                const target = e.target.closest("[data-clipboard-text]"),
159                    textToCopy = target.getAttribute("data-clipboard-text"),
160                    clipbdMsg = clipbdToast.querySelector(".toast-body");
161    
162                const init = () => {
163                    clipbdToast.classList.forEach(className => {
164                        if (className !== "toast") {
165                            clipbdToast.classList.remove(className);
166                        }
167                    });
168                    
169                    const style = target.getAttribute("data-clipboard-style") || "";
170                    if (style && !clipbdToast.classList.contains(style)) {
171                        clipbdToast.classList.add(style);
172                    }
173                };
174    
175                const onSuccess = () => {
176                    init();
177                    clipbdMsg.textContent = textToCopy + " 이(가) 복사되었습니다.";
178                    clipbdToast.classList.remove("text-bg-danger");
179                    clipboard.classList.add("active");
180                    if (timeoutId) {
181                        clearTimeout(timeoutId);
182                    }
183    
184                    timeoutId = setTimeout(() => {
185                        clipboard.classList.remove("active");
186                    }, 2500);
187                };
188    
189                const onFailure = (e) => {
190                    init();
191                    clipbdMsg.textContent = "복사에 실패하였습니다.";
192                    clipbdToast.classList.add("text-bg-danger");
193                    clipboard.classList.add("active");
194                    if (timeoutId) {
195                        clearTimeout(timeoutId);
196                    }
197    
198                    timeoutId = setTimeout(() => {
199                        clipboard.classList.remove("active");
200                    }, 2500);
201                };
202    
203                this.copyToClipboard(textToCopy, onSuccess, onFailure);
204            });
205        });
206
207        document.body.appendChild(clipboard);
208    }
209
210    /**
211     * 주어진 일 수에 해당하는 과거 날짜와 시간을 문자열 형태로 반환
212     * @param {number} numOfDays    - 계산할 과거 날짜까지의 일 수
213     * @param {boolean} includeTime - 시간을 포함할지 여부
214     * @returns {string}            - 포맷된 날짜 문자열 (예: "2024-01-25" 또는 "2024-01-25 15:30")
215      
216     * 예시 :
217     '2024-01-30'일 경우, Util.formatDate(5) -> '2024-01-25'
218     '2024-01-30 15:30'일 경우, Util.formatDate(5, true) -> '2024-01-25 15:30'
219     */
220    static formatDate(numOfDays = 0, includeTime = false) {
221        const date = new Date();
222        date.setTime(date.getTime() - (numOfDays * 24) * 60 * 60 * 1000);
223    
224        const year = date.getFullYear(),
225            month = ("0" + (1 + date.getMonth())).slice(-2),
226            day = ("0" + date.getDate()).slice(-2);
227        const dateString = year + '-' + month + '-' + day;
228    
229        let timeString = '';
230        if (includeTime) {
231            const hours = ('0' + date.getHours()).slice(-2),
232                minutes = ('0' + date.getMinutes()).slice(-2);
233            timeString = hours + ":" + minutes;
234        }
235    
236        return includeTime ? (dateString + ' ' + timeString) : dateString;
237    }
238
239    /**
240     * URL의 쿼리 매개변수 업데이트. 새로운 쿼리 매개변수를 추가하거나 기존의 매개변수를 변경
241     
242     * @param {string} key - 설정하거나 업데이트할 쿼리 매개변수의 이름
243     * @param {string} value - 쿼리 매개변수에 설정할 값
244     * 예시:
245        Util.updateQueryParams("param", "value");
246        // URL이 "http://example.com"이었다면, 변경 후 URL: "http://example.com?param=value"
247     */
248    static updateQueryParams(key, value) {
249        const currentUrl = window.location.href;
250        let newUrl;
251    
252        if (currentUrl.includes("?")) {
253            const urlParts = currentUrl.split("?"),
254                baseUrl = urlParts[0],
255                queryParams = new URLSearchParams(urlParts[1]);
256                
257            queryParams.set(key, value);  
258            newUrl = baseUrl + "?" + queryParams.toString();
259        } else {
260            newUrl = currentUrl + "?" + key + "=" + value;  
261        }
262        window.location.href = newUrl;
263    }
264
265    // 검색 기능
266    static search(selector) {
267        const type = selector.replace(/^[#.]/, '').split("-")[0];
268        const searchForm=document.querySelector(selector),
269            filterBtn=document.querySelector(".search-group-btn-filter"),
270            
271            refreshBtn=document.querySelector(".search-group-footer-btn.refresh");
272
273        // 상세 검색
274        filterBtn.addEventListener("click", ()=>{
275            searchForm.sc_word1.value = searchForm.sc_word.value || "";
276        });
277
278        searchForm.querySelector(".dropdown").addEventListener("hidden.bs.dropdown", () => {
279            searchForm.sc_word1.value = "";
280        });
281
282        searchForm.addEventListener("submit", () => {
283            if(searchForm.sc_word1.value) {
284                searchForm.sc_word.value = searchForm.sc_word1.value;
285                searchForm.sc_word1.value = "";
286            }
287        });
288        // 기간(banner)
289        if(type === "bannerpop") {
290            const periodOption=document.querySelector("#form-check-input-wrap");
291            periodOption.addEventListener("change", (e) => {
292                let period = e.target.value,
293                    resultDate = null;
294    
295                if (!period) {
296                    searchForm.sc_start_date.value = searchForm.sc_end_date.value = '';
297                    return;
298                }
299    
300                switch (period) {
301                    case 'today':
302                        resultDate = this.formatDate(0);
303                        break;
304                    case 'yesterday':
305                        resultDate = this.formatDate(1);
306                        break;
307                    default:
308                        resultDate = this.formatDate(parseInt(period, 10) - 1);
309                        break;
310                }
311                searchForm.sc_start_date.value = resultDate || "";
312                searchForm.sc_end_date.value = this.formatDate(0) || "";
313            });
314        }
315        
316        // 초기화
317        refreshBtn.addEventListener("click", ()=>{
318            searchForm.sc_word.value="";
319            searchForm.sc_word1.value="";
320            searchForm.sc_andor.value="OR";
321            searchForm.sc_word2.value="";
322
323            // 배너
324            if(type === "bannerpop") {
325                let areas=document.querySelectorAll('input[name="sc_area"]');
326                areas.forEach((e) => e.checked = e.value === "ALL");
327    
328                let dates=document.querySelectorAll('input[name="sc_date"]');
329                dates.forEach((e) => e.checked = e.value === '');
330    
331                searchForm.sc_start_date.value="";
332                searchForm.sc_end_date.value="";
333            } else if(type === "member") {
334                searchForm.stype.value = "I";
335                searchForm.querySelector("#user-level").value = "";
336                searchForm.querySelector("#user-etc-level").value = "";
337                searchForm.querySelector("#admin-level").value = "";
338                searchForm.querySelector("#user-state").value = "";
339            }
340        });
341    }
342
343    /**
344     * 드래그하여 체크박스를 선택/해제할 수 있게 합니다.
345     * @param {string} selector - 드래그 앤 드롭 기능을 적용할 선택자
346     * 예시: Util.dragCheckBox(".checkbox-container"); -> ".checkbox-container" 클래스를 가진 모든 요소 안의 체크박스에 드래그 앤 드롭 기능이 적용됩니다.
347     */
348    static dragCheckBox(selector) {
349        let isDragging = false;
350        document.querySelectorAll(selector).forEach(element => {
351            const target = element.matches("input[type='checkbox']") ? element : element.querySelector("input[type='checkbox']");
352            if (!target) return false;
353
354            target.addEventListener("click", (e) => {
355                e.preventDefault();
356            });
357            element.addEventListener("mousedown", (e) => {
358                e.preventDefault();
359                isDragging = true;
360                target.checked = !target.checked;
361            });
362
363            element.addEventListener("mouseenter", (e) => {
364                if(!isDragging) return false;
365                target.checked = !target.checked;
366            });
367
368            document.addEventListener("mouseup", (e) => {
369                isDragging = false;
370            });
371        });
372    }
373    /**
374     * 기본 AJAX 요청 수행
375     * opts 예시:
376       {
377         method: "POST", // "GET", "POST", "HEAD" 중 하나
378         func: {
379           resp: [response => response.text(), data => console.log(data)], // 응답 처리 함수 배열
380           cat: [error => alert("error!!!")] // 오류 처리 함수 배열
381         },
382         body: new FormData(form) or {"param1":"value1", "param2":"value2" ...} or key1=value1&key2=value2  // "FormData" 객체, JSON 객체 또는 쿼리 문자열
383         headers: {"Content-Type": "multipart/form-data"} // 요청 헤더
384       }
385     */
386	static ajax(url, opts={}){
387		if(!url) throw new Error('url을 입력해 주세요');
388
389		let {method="GET", func={resp:[], cat:[]}, body=null, headers={}} = opts;
390
391		const types=Array.from(new Set([typeof func, typeof headers])); // typeof body, 
392		if(types.length !== 1 || types[0] !== "object") return false;
393		if(!["GET","POST","HEAD"].includes(method)) throw new Error('허용되지 않는 HTTP Method입니다.');
394
395		//for(let [k, v] of body) console.log(k, v);
396
397		// if 'GET', disallow 'body' contents
398		if(body && method==="GET") body=null;
399
400		let response = fetch(url,  {
401										method,
402										body,
403										headers,
404									}),
405			resp = func?.resp||[],
406			cat = func?.cat||[];
407            
408		// then ...
409		if(resp.length>0){
410			resp.forEach(f => {
411				response = response.then(f);
412			});
413		}
414
415		// catch ...
416		if(cat.length>0){
417			cat.forEach(f => {
418				response = response.catch(f);
419			});
420		}
421
422		return response;
423	}
424
425    /**
426     * GET 방식으로 JSON 또는 텍스트 데이터를 요청.
427     
428     * @param {string} url - 요청할 URL
429     * @param {string} type - 응답 타입 ("json" 또는 "text")
430     * @param {Function[]} response - 응답 함수 (배열)
431     * @param {Function[]} catchs - 오류 함수 (배열)
432     * @returns {Promise} fetch 요청의 Promise 객체
433     * 호출:
434        Util.axGetData("./a.json", "json", [
435        response => response.json(),
436        data => console.log(data)
437        ],
438        [
439        error => alert("error!!!")
440        ]);
441     */
442	static axGetData(url, type="json", response=[], catchs=[]){
443		if(!url) throw new Error('url을 입력해 주세요');
444		if(!['json','text'].includes(type)) throw new Error('응답 형태는 json, text만 가능합니다.');
445
446		let headers={};
447		if(type=="json") headers={"Accept": "application/json"};
448		else			 headers={"Accept": "text/html"};
449
450		let opts={method:"GET", func:{ resp:response, cat:catchs}, body:null, headers };
451		return this.ajax(url, opts);
452	}
453
454    /**
455     * 비동기 방식으로 GET 요청 수행 axGetData 메서드와 동일하지만 async/await를 사용
456     
457     * @param {string} url - 요청할 URL
458     * @param {string} type - 응답 타입 ("json" 또는 "text")
459     * @param {Function[]} response - 응답 함수 (배열)
460     * @param {Function[]} catchs - 오류 함수 (배열)
461     * @returns {Promise} fetch 요청의 Promise 객체
462     */
463	static async axAsynGetData(url, type="json", response=[], catchs=[]){
464		return await this.axGetData(url, type, response, catchs);
465	}
466
467    /**
468     * POST 방식으로 데이터를 전송 FormData, URLSearchParams, JSON 형식의 데이터 지원 
469     
470     * @param {string} url - 요청할 URL
471     * @param {FormData|URLSearchParams|string} data - 전송할 데이터
472     * @param {Function[]} response - 응답 함수 (배열)
473     * @param {Function[]} catchs - 오류 함수 (배열)
474     * @returns {Promise} - fetch 요청의 Promise 객체
475     *
476     * 사용 예시:
477        document.querySelector("form").addEventListener("submit", e => {
478          e.preventDefault();
479          const form = e.target;
480        
481          - 방법 1: FormData 사용
482          let data = new FormData(form);                
483          data.append("param2", "value2");
484          data.append("file2", fileInput.files[0], "file2-name");
485        
486          - 방법 2: URLSearchParams 사용 (application/x-www-form-urlencoded)
487          let data = new URLSearchParams("a=1&b=2&title=제목");
488        
489          - 방법 3: JSON 데이터 사용 (application/json), backend : php://input
490          let data = { key1: "value1", key2: "value2" }; //=>변환됨 JSON.stringify({ key1: "value1", key2: "value2" });
491        
492          Util.axPostData("./a.php", data, [
493            response => response.json(),
494            data => console.log(data)
495          ], [
496            error => alert("error!!!")
497          ]);
498        });
499     */
500	static axPostData(url, data, response=[], catchs=[]){
501		if (!url) throw new Error('url을 확인해 주세요.');
502
503		let headers={};
504		if(data instanceof FormData) headers={};
505		else if(data instanceof URLSearchParams || typeof data === 'string'){
506			headers={"Content-Type": "application/x-www-form-urlencoded"}
507            if(typeof data === 'string') data=new URLSearchParams(data);
508		}else if (typeof data === 'object' && data !== null) {
509            headers = {"Content-Type": "application/json"};
510            data=JSON.stringify(data);
511        }
512
513		let opts={method:"POST", func:{ resp:response, cat:catchs}, body:data, headers };		
514		return this.ajax(url, opts);
515	}
516
517    /**
518     * 쿠키 설정
519     * @param {string} cookieName 
520     * @param {string} value 
521     * @param {number} exdays 
522     */
523    static setCookie(cookieName, value, exdays, domain, secureBool){
524        const exdate = new Date();
525        exdate.setDate(exdate.getDate() + exdays);
526        let cookieValue = encodeURIComponent(value) + "; path=/;";
527        if (exdays) {
528            cookieValue += " expires=" + exdate.toUTCString() + ";";
529        }
530        
531        if (domain) {
532            cookieValue += " domain=" + domain + ";";
533        }
534        
535        if (secureBool == "true") {
536            cookieValue += " secure";
537        }
538
539        document.cookie = cookieName + "=" + cookieValue;
540    }
541
542    /**
543     * 쿠키 읽기
544     * @param {string} cookieName 
545     * @returns string
546     */
547    static getCookie(cookieName) {
548        cookieName = cookieName + '=';
549        let cookieData = document.cookie;
550        let start = cookieData.indexOf(cookieName);
551        let cookieValue = '';
552        if (start != -1){
553            start += cookieName.length;
554            let end = cookieData.indexOf(';', start);
555            if (end == -1) end = cookieData.length;
556            cookieValue = cookieData.substring(start, end);
557        }
558        
559        return decodeURIComponent(cookieValue);
560    }
561
562    // 업로드 유효성 검사
563    static validateUploadFile(selector, onSuccess = null, onError = null) {
564        const removeUploadFile = (obj, x) => {
565            const dataTransfer = new DataTransfer();
566            let files = obj.files;
567            let fileArray = Array.from(files);
568            fileArray.forEach(file => {
569                if (file.name==x.name && file.size==x.size && file.type==x.type) return;
570                dataTransfer.items.add(file);
571            });
572            obj.files = dataTransfer.files;
573        }
574
575        const fileUploader = document.querySelectorAll(selector);
576        fileUploader.forEach(item => {
577            if (item==null || item.getAttribute("type")!="file") return;
578            item.addEventListener("change", (e) => {
579                e.stopImmediatePropagation();
580                const target = e.target;
581                Object.keys(target.files).forEach(i => {
582                    const file=target.files[i]||null;
583                    if (file===null) return;
584                    this.checkVerifyFiles(file)
585                        .then(data => {
586                            (onSuccess ? onSuccess(target, data) : true);
587                        })
588                        .catch(error => {
589                            removeUploadFile(target, file);
590                            (onError ? onError(target, error) : alert(error.message));
591                        });
592                });
593            });
594        });
595    }
596
597    // 업로드 파일 유효성 검사 (타겟 없이 직접 비교할 경우)
598    static validateUploadFileObj(file, onSuccess = null, onError = null) {
599        this.checkVerifyFiles(file)
600            .then(data=>{
601                (onSuccess ? onSuccess(data) : true);
602            })
603            .catch(error=>{
604                (onError ? onError(error) : alert(error.message));
605            });
606    }
607
608    // 파일 서명 검증 (헤더 시그니처 이용, 16진수)
609    //https://en.wikipedia.org/wiki/List_of_file_signatures
610    //https://www.garykessler.net/library/file_sigs.html
611    static checkFileSignature(uint8, ext) {
612        const fileSignature = {
613            jpg:[
614                [0xFF, 0xD8, 0xFF]
615            ],
616            png:[
617                [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
618            ],
619            gif:[
620                [0x47, 0x49, 0x46, 0x38, 0x37, 0x61],
621                [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]
622            ],
623            doc:[
624                [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], // Compound File Binary Format
625                [0xCF, 11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1, 0x00],   // Perfect Office document
626                [0xDB, 0xA5, 0x2D, 0x00],                         // Word 2.0 file
627                [0x0D, 0x44, 0x4F, 0x43]                          // DeskMate Document file
628            ],
629            xls:[
630                [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], // Compound File Binary Format
631                [0x09, 0x08, 0x10, 0x00, 0x00, 0x06, 0x05, 0x00]  // Microsoft Excel spreadsheet subheader
632            ],
633            ppt:[
634                [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], // Compound File Binary Format
635                [0x00, 0x6E, 0x1E, 0xF0],                         // Microsoft PowerPoint presentation subheader
636                [0x0F, 0x00, 0xE8, 0x03]
637            ],
638            docx:[
639                [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00], // Microsoft Office Open XML Format
640                [0x50, 0x4B, 0x03, 0x04],                         // zip file format and formats based on it
641                [0x50, 0x4B, 0x05, 0x06],
642                [0x50, 0x4B, 0x07, 0x08]
643            ],
644            xlsx:[
645                [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00], // docx 와 동일
646                [0x50, 0x4B, 0x03, 0x04],
647                [0x50, 0x4B, 0x05, 0x06],
648                [0x50, 0x4B, 0x07, 0x08]
649            ],
650            pptx:[
651                [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00], // docx 와 동일
652                [0x50, 0x4B, 0x03, 0x04],
653                [0x50, 0x4B, 0x05, 0x06],
654                [0x50, 0x4B, 0x07, 0x08]
655            ],
656            hwp:[
657                [0x48, 0x57, 0x50, 0x20, 0x44, 0x6F, 0x63, 0x75], // 구버전 (97~3.0)
658                [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]  // 신버전 (Compound File Binary Format)
659            ],
660            pdf: [
661                [0x25, 0x50, 0x44, 0x46]                          // Adobe Portable Document Format
662            ],
663            zip: [
664                [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x01, 0x00], // ZLock Pro encrypted ZIP
665                [0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45],             // PKLITE compressed ZIP archive
666                [0x50, 0x4B, 0x53, 0x70, 0x58],                   // PKSFX self-extracting executable compressed file
667                [0x50, 0x4B, 0x03, 0x04],                         // zip file format and formats based on it
668                [0x50, 0x4B, 0x05, 0x06],
669                [0x50, 0x4B, 0x07, 0x08]
670            ],
671            alz:[
672                [0x41, 0x4C, 0x5A, 0x1]
673            ]
674        };
675
676        const compareSignature = (sig, actual) => {
677            if (sig.length<=0 || actual.length<=0) return false;
678            for (let i = 0, l = sig.length; i < l; i++) {
679                if (typeof sig[i] !== 'undefined' && typeof actual[i] !== 'undefined' && sig[i] !== actual[i]) return false;
680            }
681            return true;
682        };
683
684        let result = false;
685        let isChecked = false;
686        for (const [key, value] of Object.entries(fileSignature)) {
687            if (ext=='jpeg') ext = 'jpg';
688            if (key===ext) {
689                let entry = value;
690                entry.forEach(e=>{
691                    if (compareSignature(e, uint8)===true) result = true;
692                });
693                isChecked = true;
694            }
695        }
696
697        return (isChecked===true)?result:true;
698    }
699
700    // 파일 유효성 검증 
701    static async checkVerifyFiles(file) {
702        const filename=file.name||null;
703        if (filename==null) throw new Error(`파일 정보가 없습니다. (${filename})`);
704
705        const filenames = file.name.split("."),
706            ext = filenames.pop().toLowerCase();
707
708        // 파일 확장자 체크
709        let isAllow=false;
710        Object.keys(this.fileExt).forEach(item=>{
711            if (this.isAllowFileExt(file.name, item)) isAllow=true;
712        });
713        if (isAllow===false) throw new Error(`허용된 파일 유형이 아닙니다. (${filename})`);
714
715        // 파일 이름 (.) 체크
716        if (filenames.length>1) {
717            throw new Error(`파일명에서 확장자의 점(.)을 제외한 것을 제거하여 주세요. (${filename})`);
718        }
719
720        // 파일 타입 체크
721        const checkHeader=(headers)=>{
722                return (buffers, options={
723                    offset: 0
724                })=>headers.every((header,index)=>header === buffers[options.offset + index]);
725            },
726            readBuffer=(file, start=0, end=2)=>{
727                return new Promise((resolve,reject)=>{
728                    const reader=new FileReader();
729                    reader.onload=()=>{
730                        resolve(reader.result);
731                    };
732                    reader.onerror = reject;
733                    reader.readAsArrayBuffer(file.slice(start, end));
734                });
735            },
736            getMimetype=(file, uint8)=>{
737                // 특정 PNG 파일은 Mime Type 을 읽지 못해서 파일의 8바이트를 읽어 들여 체크
738                const isPNG = checkHeader([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
739                if (uint8!==null && isPNG(uint8)) return "image/png";
740                else {
741                    if (file.type!="") return file.type;
742                    else {
743                        const ext = file.name.split(".").pop().toLowerCase();
744                        switch (ext) {
745                            case 'alz': return 'application/alzip'; break;
746                            case 'hwp': return 'application/vnd.hancom.hwp'; break;
747                            case 'hwpx': return 'application/vnd.hancom.hwpx'; break;
748                            default: return ''; break;
749                        }
750                    }
751                }
752            },
753            getMimetypeExtension=(mimetype)=>{
754                if (/jpeg/i.test(mimetype)) {
755                    return "jpg";
756                } else if (/alzip/i.test(mimetype)) {
757                    return "alz";
758                } else if (/zip/i.test(mimetype)) {
759                    return "zip";
760                } else if (/spreadsheetml/i.test(mimetype)) {
761                    return "xlsx";
762                } else if (/excel/i.test(mimetype)) {
763                    return "xls";
764                } else if (/wordprocessingml/i.test(mimetype)) {
765                    return "docx";
766                } else if (/msword/i.test(mimetype)) {
767                    return "doc";
768                } else if (/presentationml/i.test(mimetype)) {
769                    return "pptx";
770                } else if (/powerpoint/i.test(mimetype)) {
771                    return "ppt";
772                } else if (/hwpx/i.test(mimetype)) {
773                    return "hwpx";
774                } else if (/hwp/i.test(mimetype)) {
775                    return "hwp";
776                } else if (/text\/plain/i.test(mimetype)) {
777                    return "txt";
778                } else {
779                    const mc=mimetype.split("/");
780                    return (mc[1]!=null)?mc[1]:null;
781                }
782            };
783
784        // 10MB 이상인 경우 파일을 읽지 않고 브라우저 정보를 이용하여 비교
785        const buffMaxFilesize = 10;
786        const maxUploadSizeBytes = parseInt(buffMaxFilesize) * 1024 * 1024;
787
788        let mimetype = '',
789            mimeExt = '',
790            uint8Array = [],
791            isReadBuffer = false;
792
793        if (file.size>=maxUploadSizeBytes) {
794            mimetype = getMimetype(file, null);
795            mimeExt = getMimetypeExtension(mimetype);
796        } else {
797            const buffers = await readBuffer(file, 0, 8);
798            uint8Array = new Uint8Array(buffers);
799            mimetype = getMimetype(file, uint8Array);
800            mimeExt = getMimetypeExtension(mimetype);
801            isReadBuffer = true;
802        }
803
804        if (!mimetype) {
805            throw new Error(`확인할 수 없는 파일 유형입니다. (${filename})`);
806        }
807
808        if ((ext!='jpeg'&&mimeExt!=ext) || (ext=='jpeg'&&mimeExt!='jpg')) {
809            throw new Error(`파일 타입(${mimetype})과 확장자가 일치하지 않으니 ${mimeExt}형태로 변경하여 주세요. (${filename})`);
810        }
811
812        if (isReadBuffer===true && this.checkFileSignature(uint8Array, ext)!==true) {
813            throw new Error(`파일 검증 오류 입니다. (${filename})`);
814        }
815
816        return file;
817    }
818}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.