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.