PageSourceSearch

https://cdn.okinews.com/script/Util.js

js okinews.com collected 2026-10-02 19:28:58 UTC 38,331 bytes, 933 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", "hwpx"] };
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    // toast 메세지 출력 
135    static showMessage(msg, delay = 2, style = []) { 
136        const winTop =  window.top;
137
138        let clipboard=winTop.document.querySelector(".toast-container"); 
139        if (clipboard===null) { 
140            clipboard=document.createElement("div");
141            clipboard.classList.add("toast-container");
142            clipboard.classList.add("top");
143        }
144
145        const clipbdToast = document.createElement("div");
146        clipbdToast.classList.add("toast");
147        clipbdToast.classList.add("toast-horz");
148        clipbdToast.setAttribute("role", "alert");
149        clipbdToast.setAttribute("aria-live", "assertive");
150        clipbdToast.setAttribute("aria-atomic", "true");
151        clipbdToast.classList.add("marbtm-5");
152
153        const clipbdToastBody = document.createElement("div");
154        clipbdToastBody.className = "toast-body";
155
156        const btnClose = document.createElement("button");
157        btnClose.type = "button";
158        btnClose.className = "btn-close";
159        btnClose.onclick=(e)=>$(clipbdToast).fadeOut(()=>$(clipbdToast).remove());
160
161        clipbdToast.appendChild(clipbdToastBody);
162        clipbdToast.appendChild(btnClose);
163        
164        if (clipboard.classList.contains("top")) { 
165            clipboard.insertAdjacentElement("beforeend", clipbdToast);
166        } else {
167            clipboard.insertAdjacentElement("afterbegin", clipbdToast);
168        }
169
170        const clipbdMsg = clipbdToast.querySelector(".toast-body");
171
172        if (style && style.length > 0) {
173            style.forEach(css=>{
174                if (css && !clipbdToast.classList.contains(css)) {
175                    clipbdToast.classList.add(css);
176                }
177            });
178        }
179        
180        clipbdMsg.textContent = msg;
181        clipbdToast.classList.add("show");
182
183        winTop.document.body.appendChild(clipboard);
184
185        let delayTime = parseInt(delay) * 1000;
186        setTimeout(()=>$(clipbdToast).fadeOut(()=>$(clipbdToast).remove()), delayTime);
187    }
188
189    // 페이지 이동 또는 출력된 메세지들을 전부 닫을때 
190    static closeAllMessage() {
191        const winTop =  window.top;
192        const clipboard = winTop.document.querySelector(".toast-container");
193        if (clipboard!=null) {
194            clipboard.querySelectorAll(".toast").forEach(child=>$(child).remove());
195        }
196    }
197
198    // 클립보드 복사 후 toast 메세지 출력
199    static copyToClipboardToast(selector, msg = '', source = '') { 
200        if (selector==null || selector==undefined || source=='') return; 
201
202        msg = (msg!="")?msg:"복사되었습니다. 원하는 곳에 붙여넣기하여 주세요.";
203
204        let style = [];
205        if (selector && selector.getAttribute("data-clipboard-style")) { 
206            const clipStyle = selector.getAttribute("data-clipboard-style") || ""; 
207            style = clipStyle.split(" ") || [];
208            style = style.filter((element, i) => element != '');
209        }
210
211        let delayTime = 2; 
212        if (selector && selector.getAttribute("data-clipboard-delay")) { 
213            delayTime = selector.getAttribute("data-clipboard-delay") || delayTime;
214        }
215       
216        const onSuccess = () => {
217            style.push("toast-primary");
218            this.showMessage(msg, delayTime, style);
219        };
220
221        const onFailure = (e) => {
222            style.push("toast-danger");
223            this.showMessage("복사에 실패하였습니다.", delayTime, style);
224        };
225
226        this.copyToClipboard(source, onSuccess, onFailure);
227    }
228    
229    // 클립보드 toast 메세지 로드
230    static createClipboardToast(target = '[data-clipboard-action]') {
231        const clipboardView = document.querySelectorAll(target);
232        clipboardView.forEach(element => {
233            element.addEventListener("click", (e) => {
234                const target = e.target, 
235                    targetData = target.closest("[data-clipboard-action]");
236                if (targetData===null) return;
237
238                const message = targetData.getAttribute("data-clipboard-message") || "";
239
240                let text = '';
241                if (targetData && targetData.getAttribute("data-clipboard-text")) {
242                    text = targetData.getAttribute("data-clipboard-text");
243                }
244                else if (targetData && targetData.getAttribute("data-clipboard-target")) {
245                    const targetSelector = targetData.getAttribute("data-clipboard-target");
246                    const targetElement = document.querySelector(targetSelector);
247                    text = targetElement ? (targetElement.value || targetElement.textContent) : '';
248                }
249
250                this.copyToClipboardToast(target, message, text);
251            });
252        });
253    }
254        
255    static createClipboard(target=".clipboard-view") {
256        let timeoutId;
257
258        const clipboard = document.createElement("div");
259        clipboard.className = "clipboard";
260        const clipbdToast = document.createElement("div");
261        clipbdToast.id = "clipbd-toast";
262        clipbdToast.className = "toast"
263        const clipbdToastBody = document.createElement("div");
264        clipbdToastBody.className = "toast-body";
265        const btnClose = document.createElement("button");
266        btnClose.type = "button";
267        btnClose.className = "btn-close";
268        btnClose.onclick=e=>{ clipboard.classList.remove("active"); };
269
270        clipbdToast.appendChild(clipbdToastBody);
271        clipbdToast.appendChild(btnClose);
272        clipboard.appendChild(clipbdToast);
273
274        const clipboardView = document.querySelectorAll(target);
275
276        clipboardView.forEach(element => {
277            element.addEventListener("click", (e) => {
278                const target = e.target.closest("[data-clipboard-text]"),
279                    textToCopy = target.getAttribute("data-clipboard-text"),
280                    clipbdMsg = clipbdToast.querySelector(".toast-body");
281    
282                const init = () => {
283                    clipbdToast.classList.forEach(className => {
284                        if (className !== "toast") {
285                            clipbdToast.classList.remove(className);
286                        }
287                    });
288                    
289                    const style = target.getAttribute("data-clipboard-style") || "";
290                    if (style && !clipbdToast.classList.contains(style)) {
291                        clipbdToast.classList.add(style);
292                    }
293                };
294    
295                const onSuccess = () => {
296                    init();
297                    clipbdMsg.textContent = textToCopy + " 이(가) 복사되었습니다.";
298                    clipbdToast.classList.remove("text-bg-danger");
299                    clipboard.classList.add("active");
300                    if (timeoutId) {
301                        clearTimeout(timeoutId);
302                    }
303    
304                    timeoutId = setTimeout(() => {
305                        clipboard.classList.remove("active");
306                    }, 2500);
307                };
308    
309                const onFailure = (e) => {
310                    init();
311                    clipbdMsg.textContent = "복사에 실패하였습니다.";
312                    clipbdToast.classList.add("text-bg-danger");
313                    clipboard.classList.add("active");
314                    if (timeoutId) {
315                        clearTimeout(timeoutId);
316                    }
317    
318                    timeoutId = setTimeout(() => {
319                        clipboard.classList.remove("active");
320                    }, 2500);
321                };
322    
323                this.copyToClipboard(textToCopy, onSuccess, onFailure);
324            });
325        });
326
327        document.body.appendChild(clipboard);
328    }
329
330    /**
331     * 주어진 일 수에 해당하는 과거 날짜와 시간을 문자열 형태로 반환
332     * @param {number} numOfDays    - 계산할 과거 날짜까지의 일 수
333     * @param {boolean} includeTime - 시간을 포함할지 여부
334     * @returns {string}            - 포맷된 날짜 문자열 (예: "2024-01-25" 또는 "2024-01-25 15:30")
335      
336     * 예시 :
337     '2024-01-30'일 경우, Util.formatDate(5) -> '2024-01-25'
338     '2024-01-30 15:30'일 경우, Util.formatDate(5, true) -> '2024-01-25 15:30'
339     */
340    static formatDate(numOfDays = 0, includeTime = false) {
341        const date = new Date();
342        date.setTime(date.getTime() - (numOfDays * 24) * 60 * 60 * 1000);
343    
344        const year = date.getFullYear(),
345            month = ("0" + (1 + date.getMonth())).slice(-2),
346            day = ("0" + date.getDate()).slice(-2);
347        const dateString = year + '-' + month + '-' + day;
348    
349        let timeString = '';
350        if (includeTime) {
351            const hours = ('0' + date.getHours()).slice(-2),
352                minutes = ('0' + date.getMinutes()).slice(-2);
353            timeString = hours + ":" + minutes;
354        }
355    
356        return includeTime ? (dateString + ' ' + timeString) : dateString;
357    }
358
359    /**
360     * URL의 쿼리 매개변수 업데이트. 새로운 쿼리 매개변수를 추가하거나 기존의 매개변수를 변경
361     
362     * @param {string} key - 설정하거나 업데이트할 쿼리 매개변수의 이름
363     * @param {string} value - 쿼리 매개변수에 설정할 값
364     * 예시:
365        Util.updateQueryParams("param", "value");
366        // URL이 "http://example.com"이었다면, 변경 후 URL: "http://example.com?param=value"
367     */
368    static updateQueryParams(key, value) {
369        const currentUrl = window.location.href;
370        let newUrl;
371    
372        if (currentUrl.includes("?")) {
373            const urlParts = currentUrl.split("?"),
374                baseUrl = urlParts[0],
375                queryParams = new URLSearchParams(urlParts[1]);
376                
377            queryParams.set(key, value);  
378            newUrl = baseUrl + "?" + queryParams.toString();
379        } else {
380            newUrl = currentUrl + "?" + key + "=" + value;  
381        }
382        window.location.href = newUrl;
383    }
384
385    // 검색 기능
386    static search(selector) {
387        const type = selector.replace(/^[#.]/, '').split("-")[0];
388        const searchForm=document.querySelector(selector),
389            filterBtn=document.querySelector(".search-group-btn-filter"),
390            
391            refreshBtn=document.querySelector(".search-group-footer-btn.refresh");
392
393        // 상세 검색
394        filterBtn.addEventListener("click", ()=>{
395            searchForm.sc_word1.value = searchForm.sc_word.value || "";
396        });
397
398        searchForm.querySelector(".dropdown").addEventListener("hidden.bs.dropdown", () => {
399            searchForm.sc_word1.value = "";
400        });
401
402        searchForm.addEventListener("submit", () => {
403            if(searchForm.sc_word1.value) {
404                searchForm.sc_word.value = searchForm.sc_word1.value;
405                searchForm.sc_word1.value = "";
406            }
407        });
408        // 기간(banner)
409        if(type === "bannerpop") {
410            const periodOption=document.querySelector("#form-check-input-wrap");
411            periodOption.addEventListener("change", (e) => {
412                let period = e.target.value,
413                    resultDate = null;
414    
415                if (!period) {
416                    searchForm.sc_start_date.value = searchForm.sc_end_date.value = '';
417                    return;
418                }
419    
420                switch (period) {
421                    case 'today':
422                        resultDate = this.formatDate(0);
423                        break;
424                    case 'yesterday':
425                        resultDate = this.formatDate(1);
426                        break;
427                    default:
428                        resultDate = this.formatDate(parseInt(period, 10) - 1);
429                        break;
430                }
431                searchForm.sc_start_date.value = resultDate || "";
432                searchForm.sc_end_date.value = this.formatDate(0) || "";
433            });
434        }
435        
436        // 초기화
437        refreshBtn.addEventListener("click", ()=>{
438            searchForm.sc_word.value="";
439            searchForm.sc_word1.value="";
440            searchForm.sc_andor.value="OR";
441            searchForm.sc_word2.value="";
442
443            // 배너
444            if(type === "bannerpop") {
445                let areas=document.querySelectorAll('input[name="sc_area"]');
446                areas.forEach((e) => e.checked = e.value === "ALL");
447    
448                let dates=document.querySelectorAll('input[name="sc_date"]');
449                dates.forEach((e) => e.checked = e.value === '');
450    
451                searchForm.sc_start_date.value="";
452                searchForm.sc_end_date.value="";
453            } else if(type === "member") {
454                searchForm.stype.value = "I";
455                searchForm.querySelector("#user-level").value = "";
456                searchForm.querySelector("#user-etc-level").value = "";
457                searchForm.querySelector("#admin-level").value = "";
458                searchForm.querySelector("#user-state").value = "";
459            }
460        });
461    }
462
463    /**
464     * 드래그하여 체크박스를 선택/해제할 수 있게 합니다.
465     * @param {string} selector - 드래그 앤 드롭 기능을 적용할 선택자
466     * 예시: Util.dragCheckBox(".checkbox-container"); -> ".checkbox-container" 클래스를 가진 모든 요소 안의 체크박스에 드래그 앤 드롭 기능이 적용됩니다.
467     */
468    static dragCheckBox(selector) {
469        let isDragging = false;
470        document.querySelectorAll(selector).forEach(element => {
471            const target = element.matches("input[type='checkbox']") ? element : element.querySelector("input[type='checkbox']");
472            if (!target) return false;
473
474            target.addEventListener("click", (e) => {
475                e.preventDefault();
476            });
477            element.addEventListener("mousedown", (e) => {
478                e.preventDefault();
479                isDragging = true;
480                target.checked = !target.checked;
481            });
482
483            element.addEventListener("mouseenter", (e) => {
484                if(!isDragging) return false;
485                target.checked = !target.checked;
486            });
487
488            document.addEventListener("mouseup", (e) => {
489                isDragging = false;
490            });
491        });
492    }
493    /**
494     * 기본 AJAX 요청 수행
495     * opts 예시:
496       {
497         method: "POST", // "GET", "POST", "HEAD" 중 하나
498         func: {
499           resp: [response => response.text(), data => console.log(data)], // 응답 처리 함수 배열
500           cat: [error => alert("error!!!")] // 오류 처리 함수 배열
501         },
502         body: new FormData(form) or {"param1":"value1", "param2":"value2" ...} or key1=value1&key2=value2  // "FormData" 객체, JSON 객체 또는 쿼리 문자열
503         headers: {"Content-Type": "multipart/form-data"} // 요청 헤더
504       }
505     */
506	static ajax(url, opts={}){
507		if(!url) throw new Error('url을 입력해 주세요');
508
509		let {method="GET", func={resp:[], cat:[]}, body=null, headers={}} = opts;
510
511		const types=Array.from(new Set([typeof func, typeof headers])); // typeof body, 
512		if(types.length !== 1 || types[0] !== "object") return false;
513		if(!["GET","POST","HEAD"].includes(method)) throw new Error('허용되지 않는 HTTP Method입니다.');
514
515		//for(let [k, v] of body) console.log(k, v);
516
517		// if 'GET', disallow 'body' contents
518		if(body && method==="GET") body=null;
519
520		let response = fetch(url,  {
521										method,
522										body,
523										headers,
524									}),
525			resp = func?.resp||[],
526			cat = func?.cat||[];
527            
528		// then ...
529		if(resp.length>0){
530			resp.forEach(f => {
531				response = response.then(f);
532			});
533		}
534
535		// catch ...
536		if(cat.length>0){
537			cat.forEach(f => {
538				response = response.catch(f);
539			});
540		}
541
542		return response;
543	}
544
545    /**
546     * GET 방식으로 JSON 또는 텍스트 데이터를 요청.
547     
548     * @param {string} url - 요청할 URL
549     * @param {string} type - 응답 타입 ("json" 또는 "text")
550     * @param {Function[]} response - 응답 함수 (배열)
551     * @param {Function[]} catchs - 오류 함수 (배열)
552     * @returns {Promise} fetch 요청의 Promise 객체
553     * 호출:
554        Util.axGetData("./a.json", "json", [
555        response => response.json(),
556        data => console.log(data)
557        ],
558        [
559        error => alert("error!!!")
560        ]);
561     */
562	static axGetData(url, type="json", response=[], catchs=[]){
563		if(!url) throw new Error('url을 입력해 주세요');
564		if(!['json','text'].includes(type)) throw new Error('응답 형태는 json, text만 가능합니다.');
565
566		let headers={};
567		if(type=="json") headers={"Accept": "application/json"};
568		else			 headers={"Accept": "text/html"};
569
570		let opts={method:"GET", func:{ resp:response, cat:catchs}, body:null, headers };
571		return this.ajax(url, opts);
572	}
573
574    /**
575     * 비동기 방식으로 GET 요청 수행 axGetData 메서드와 동일하지만 async/await를 사용
576     
577     * @param {string} url - 요청할 URL
578     * @param {string} type - 응답 타입 ("json" 또는 "text")
579     * @param {Function[]} response - 응답 함수 (배열)
580     * @param {Function[]} catchs - 오류 함수 (배열)
581     * @returns {Promise} fetch 요청의 Promise 객체
582     */
583	static async axAsynGetData(url, type="json", response=[], catchs=[]){
584		return await this.axGetData(url, type, response, catchs);
585	}
586
587    /**
588     * POST 방식으로 데이터를 전송 FormData, URLSearchParams, JSON 형식의 데이터 지원 
589     
590     * @param {string} url - 요청할 URL
591     * @param {FormData|URLSearchParams|string} data - 전송할 데이터
592     * @param {Function[]} response - 응답 함수 (배열)
593     * @param {Function[]} catchs - 오류 함수 (배열)
594     * @returns {Promise} - fetch 요청의 Promise 객체
595     *
596     * 사용 예시:
597        document.querySelector("form").addEventListener("submit", e => {
598          e.preventDefault();
599          const form = e.target;
600        
601          - 방법 1: FormData 사용
602          let data = new FormData(form);                
603          data.append("param2", "value2");
604          data.append("file2", fileInput.files[0], "file2-name");
605        
606          - 방법 2: URLSearchParams 사용 (application/x-www-form-urlencoded)
607          let data = new URLSearchParams("a=1&b=2&title=제목");
608        
609          - 방법 3: JSON 데이터 사용 (application/json), backend : php://input
610          let data = { key1: "value1", key2: "value2" }; //=>변환됨 JSON.stringify({ key1: "value1", key2: "value2" });
611        
612          Util.axPostData("./a.php", data, [
613            response => response.json(),
614            data => console.log(data)
615          ], [
616            error => alert("error!!!")
617          ]);
618        });
619     */
620	static axPostData(url, data, response=[], catchs=[]){
621		if (!url) throw new Error('url을 확인해 주세요.');
622
623		let headers={};
624		if(data instanceof FormData) headers={};
625		else if(data instanceof URLSearchParams || typeof data === 'string'){
626			headers={"Content-Type": "application/x-www-form-urlencoded"}
627            if(typeof data === 'string') data=new URLSearchParams(data);
628		}else if (typeof data === 'object' && data !== null) {
629            headers = {"Content-Type": "application/json"};
630            data=JSON.stringify(data);
631        }
632
633		let opts={method:"POST", func:{ resp:response, cat:catchs}, body:data, headers };		
634		return this.ajax(url, opts);
635	}
636
637    /**
638     * 쿠키 설정
639     * @param {string} cookieName 
640     * @param {string} value 
641     * @param {number} exdays 
642     */
643    static setCookie(cookieName, value, exdays, domain, secureBool){
644        const exdate = new Date();
645        exdate.setDate(exdate.getDate() + exdays);
646        let cookieValue = encodeURIComponent(value) + "; path=/;";
647        if (exdays) {
648            cookieValue += " expires=" + exdate.toUTCString() + ";";
649        }
650        
651        if (domain) {
652            cookieValue += " domain=" + domain + ";";
653        }
654        
655        if (secureBool == "true") {
656            cookieValue += " secure";
657        }
658
659        document.cookie = cookieName + "=" + cookieValue;
660    }
661
662    /**
663     * 쿠키 읽기
664     * @param {string} cookieName 
665     * @returns string
666     */
667    static getCookie(cookieName) {
668        cookieName = cookieName + '=';
669        let cookieData = document.cookie;
670        let start = cookieData.indexOf(cookieName);
671        let cookieValue = '';
672        if (start != -1){
673            start += cookieName.length;
674            let end = cookieData.indexOf(';', start);
675            if (end == -1) end = cookieData.length;
676            cookieValue = cookieData.substring(start, end);
677        }
678        
679        return decodeURIComponent(cookieValue);
680    }
681
682    // 업로드 유효성 검사
683    static validateUploadFile(selector, onSuccess = null, onError = null) {
684        const removeUploadFile = (obj, x) => {
685            const dataTransfer = new DataTransfer();
686            let files = obj.files;
687            let fileArray = Array.from(files);
688            fileArray.forEach(file => {
689                if (file.name==x.name && file.size==x.size && file.type==x.type) return;
690                dataTransfer.items.add(file);
691            });
692            obj.files = dataTransfer.files;
693        }
694
695        const fileUploader = document.querySelectorAll(selector);
696        fileUploader.forEach(item => {
697            if (item==null || item.getAttribute("type")!="file") return;
698            item.addEventListener("change", (e) => {
699                e.stopImmediatePropagation();
700                const target = e.target;
701                Object.keys(target.files).forEach(i => {
702                    const file=target.files[i]||null;
703                    if (file===null) return;
704                    this.checkVerifyFiles(file)
705                        .then(data => {
706                            (onSuccess ? onSuccess(target, data) : true);
707                        })
708                        .catch(error => {
709                            removeUploadFile(target, file);
710                            (onError ? onError(target, error) : alert(error.message));
711                        });
712                });
713            });
714        });
715    }
716
717    // 업로드 파일 유효성 검사 (타겟 없이 직접 비교할 경우)
718    static validateUploadFileObj(file, onSuccess = null, onError = null) {
719        this.checkVerifyFiles(file)
720            .then(data=>{
721                (onSuccess ? onSuccess(data) : true);
722            })
723            .catch(error=>{
724                (onError ? onError(error) : alert(error.message));
725            });
726    }
727
728    // 파일 서명 검증 (헤더 시그니처 이용, 16진수)
729    //https://en.wikipedia.org/wiki/List_of_file_signatures
730    //https://www.garykessler.net/library/file_sigs.html
731    static checkFileSignature(uint8, ext) {
732        const fileSignature = {
733            jpg:[
734                [0xFF, 0xD8, 0xFF]
735            ],
736            png:[
737                [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
738            ],
739            gif:[
740                [0x47, 0x49, 0x46, 0x38, 0x37, 0x61],
741                [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]
742            ],
743            doc:[
744                [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], // Compound File Binary Format
745                [0xCF, 11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1, 0x00],   // Perfect Office document
746                [0xDB, 0xA5, 0x2D, 0x00],                         // Word 2.0 file
747                [0x0D, 0x44, 0x4F, 0x43]                          // DeskMate Document file
748            ],
749            xls:[
750                [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], // Compound File Binary Format
751                [0x09, 0x08, 0x10, 0x00, 0x00, 0x06, 0x05, 0x00]  // Microsoft Excel spreadsheet subheader
752            ],
753            ppt:[
754                [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], // Compound File Binary Format
755                [0x00, 0x6E, 0x1E, 0xF0],                         // Microsoft PowerPoint presentation subheader
756                [0x0F, 0x00, 0xE8, 0x03]
757            ],
758            docx:[
759                [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00], // Microsoft Office Open XML Format
760                [0x50, 0x4B, 0x03, 0x04],                         // zip file format and formats based on it
761                [0x50, 0x4B, 0x05, 0x06],
762                [0x50, 0x4B, 0x07, 0x08]
763            ],
764            xlsx:[
765                [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00], // docx 와 동일
766                [0x50, 0x4B, 0x03, 0x04],
767                [0x50, 0x4B, 0x05, 0x06],
768                [0x50, 0x4B, 0x07, 0x08]
769            ],
770            pptx:[
771                [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00], // docx 와 동일
772                [0x50, 0x4B, 0x03, 0x04],
773                [0x50, 0x4B, 0x05, 0x06],
774                [0x50, 0x4B, 0x07, 0x08]
775            ],
776            hwp:[
777                [0x48, 0x57, 0x50, 0x20, 0x44, 0x6F, 0x63, 0x75], // 구버전 (97~3.0)
778                [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]  // 신버전 (Compound File Binary Format)
779            ],
780            pdf: [
781                [0x25, 0x50, 0x44, 0x46]                          // Adobe Portable Document Format
782            ],
783            zip: [
784                [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x01, 0x00], // ZLock Pro encrypted ZIP
785                [0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45],             // PKLITE compressed ZIP archive
786                [0x50, 0x4B, 0x53, 0x70, 0x58],                   // PKSFX self-extracting executable compressed file
787                [0x50, 0x4B, 0x03, 0x04],                         // zip file format and formats based on it
788                [0x50, 0x4B, 0x05, 0x06],
789                [0x50, 0x4B, 0x07, 0x08]
790            ],
791            alz:[
792                [0x41, 0x4C, 0x5A, 0x1]
793            ]
794        };
795
796        const compareSignature = (sig, actual) => {
797            if (sig.length<=0 || actual.length<=0) return false;
798            for (let i = 0, l = sig.length; i < l; i++) {
799                if (typeof sig[i] !== 'undefined' && typeof actual[i] !== 'undefined' && sig[i] !== actual[i]) return false;
800            }
801            return true;
802        };
803
804        let result = false;
805        let isChecked = false;
806        for (const [key, value] of Object.entries(fileSignature)) {
807            if (ext=='jpeg') ext = 'jpg';
808            if (key===ext) {
809                let entry = value;
810                entry.forEach(e=>{
811                    if (compareSignature(e, uint8)===true) result = true;
812                });
813                isChecked = true;
814            }
815        }
816
817        return (isChecked===true)?result:true;
818    }
819
820    // 파일 유효성 검증 
821    static async checkVerifyFiles(file) {
822        const filename=file.name||null;
823        if (filename==null) throw new Error(`파일 정보가 없습니다. (${filename})`);
824
825        const filenames = file.name.split("."),
826            ext = filenames.pop().toLowerCase();
827
828        // 파일 확장자 체크
829        let isAllow=false;
830        Object.keys(this.fileExt).forEach(item=>{
831            if (this.isAllowFileExt(file.name, item)) isAllow=true;
832        });
833        if (isAllow===false) throw new Error(`허용된 파일 유형이 아닙니다. (${filename})`);
834
835        // 파일 타입 체크
836        const checkHeader=(headers)=>{
837                return (buffers, options={
838                    offset: 0
839                })=>headers.every((header,index)=>header === buffers[options.offset + index]);
840            },
841            readBuffer=(file, start=0, end=2)=>{
842                return new Promise((resolve,reject)=>{
843                    const reader=new FileReader();
844                    reader.onload=()=>{
845                        resolve(reader.result);
846                    };
847                    reader.onerror = reject;
848                    reader.readAsArrayBuffer(file.slice(start, end));
849                });
850            },
851            getMimetype=(file, uint8)=>{
852                // 특정 PNG 파일은 Mime Type 을 읽지 못해서 파일의 8바이트를 읽어 들여 체크
853                const isPNG = checkHeader([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
854                if (uint8!==null && isPNG(uint8)) return "image/png";
855                else {
856                    if (file.type!="") return file.type;
857                    else {
858                        const ext = file.name.split(".").pop().toLowerCase();
859                        switch (ext) {
860                            case 'alz': return 'application/alzip'; break;
861                            case 'hwp': return 'application/vnd.hancom.hwp'; break;
862                            case 'hwpx': return 'application/vnd.hancom.hwpx'; break;
863                            default: return ''; break;
864                        }
865                    }
866                }
867            },
868            getMimetypeExtension=(mimetype)=>{
869                if (/jpeg/i.test(mimetype)) {
870                    return "jpg";
871                } else if (/alzip/i.test(mimetype)) {
872                    return "alz";
873                } else if (/zip/i.test(mimetype)) {
874                    return "zip";
875                } else if (/spreadsheetml/i.test(mimetype)) {
876                    return "xlsx";
877                } else if (/excel/i.test(mimetype)) {
878                    return "xls";
879                } else if (/wordprocessingml/i.test(mimetype)) {
880                    return "docx";
881                } else if (/msword/i.test(mimetype)) {
882                    return "doc";
883                } else if (/presentationml/i.test(mimetype)) {
884                    return "pptx";
885                } else if (/powerpoint/i.test(mimetype)) {
886                    return "ppt";
887                } else if (/hwpx/i.test(mimetype)) {
888                    return "hwpx";
889                } else if (/hwp/i.test(mimetype)) {
890                    return "hwp";
891                } else if (/text\/plain/i.test(mimetype)) {
892                    return "txt";
893                } else {
894                    const mc=mimetype.split("/");
895                    return (mc[1]!=null)?mc[1]:null;
896                }
897            };
898
899        // 10MB 이상인 경우 파일을 읽지 않고 브라우저 정보를 이용하여 비교
900        const buffMaxFilesize = 10;
901        const maxUploadSizeBytes = parseInt(buffMaxFilesize) * 1024 * 1024;
902
903        let mimetype = '',
904            mimeExt = '',
905            uint8Array = [],
906            isReadBuffer = false;
907
908        if (file.size>=maxUploadSizeBytes) {
909            mimetype = getMimetype(file, null);
910            mimeExt = getMimetypeExtension(mimetype);
911        } else {
912            const buffers = await readBuffer(file, 0, 8);
913            uint8Array = new Uint8Array(buffers);
914            mimetype = getMimetype(file, uint8Array);
915            mimeExt = getMimetypeExtension(mimetype);
916            isReadBuffer = true;
917        }
918
919        if (!mimetype) {
920            throw new Error(`확인할 수 없는 파일 유형입니다. (${filename})`);
921        }
922
923        if ((ext!='jpeg'&&mimeExt!=ext) || (ext=='jpeg'&&mimeExt!='jpg')) {
924            throw new Error(`파일 타입(${mimetype})과 확장자가 일치하지 않으니 ${mimeExt}형태로 변경하여 주세요. (${filename})`);
925        }
926
927        if (isReadBuffer===true && this.checkFileSignature(uint8Array, ext)!==true) {
928            throw new Error(`파일 검증 오류 입니다. (${filename})`);
929        }
930
931        return file;
932    }
933}

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.