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.