1 2// null or undefined ì²´í¬ í¨ì 3function isNull(value){ 4 return value === null || value === undefined || (typeof value === 'string' && value.trim() === ""); 5} 6 7function isEmptyObject(obj) { 8 return obj && Object.keys(obj).length === 0 && obj.constructor === Object; 9} 10 11// ê°ì²´ ëë ë°°ì´ì ê¹ì ë³µì¬íë¤. 12function deepClone(obj) { 13 return JSON.parse(JSON.stringify(obj)); 14} 15 16// ë°°ì´ìì ì¤ë³µí목ì ì ê±°íë¤. 17function uniqueArray(arr) { 18 return [...new Set(arr)]; 19} 20 21// text를 í´ë¦½ë³´ëë¡ ë³µì¬í´ì£¼ë í¨ì 22function copyToClipboard(text) { 23 return navigator.clipboard.writeText(text); 24} 25 26// íì¬ íì´ì§ URLì í´ë¦½ë³´ëì ë³µì¬íë ê¸°ë¥ 27function copyCurrentUrlToClipboard() { 28 const currentUrl = window.location.href; 29 30 navigator.clipboard.writeText(currentUrl) 31 .then(() => { 32 alert("URLì´ í´ë¦½ë³´ëì ë³µì¬ëììµëë¤!"); 33 }) 34 .catch(err => { 35 console.error("í´ë¦½ë³´ë ë³µì¬ ì¤í¨:", err); 36 }); 37} 38 39// URLì 쿼리 매ê°ë³ì를 ê°ì²´ë¡ ë³í 40function getQueryParams() { 41 return Object.fromEntries(new URLSearchParams(window.location.search)); 42} 43 44// íì´ì§ë¥¼ ì¤í¬ë¡¤íë í¨ì, 0ì´ë©´ 맨 ìë¡ ì¤í¬ë¡¤íë¤. 45function scrollToTop(top) { 46 window.scrollTo({ top: top, behavior: 'smooth' }); 47} 48 49// 문ìì´(str)ì ì§ì ë 길ì´(length)ë¡ ì른ë¤ì ...를 ì¶ê°íë¤. 50function textTruncate(str, length) { 51 return str.length > length ? str.slice(0, length) + '...' : str; 52} 53 54// 문ìì´ì 첫 ê¸ì를 ë문ìë¡ ë³ííë¤. 55function capitalize(str) { 56 return str.charAt(0).toUpperCase() + str.slice(1); 57} 58 59// ip4 ê²ì¦ 60function isValidIPv4(ip) { 61 if (typeof ip !== 'string') return false; 62 63 const parts = ip.trim().split('.'); 64 if (parts.length !== 4) return false; 65 66 for (const part of parts) { 67 // ë¹ ë¬¸ìì´ or ì«ìê° ìë 68 if (!/^\d+$/.test(part)) return false; 69 70 const num = Number(part); 71 if (num < 0 || num > 255) return false; 72 73 // "01", "001" ê°ì ì í 0 ê¸ì§ (ë¨, "0"ì íì©) 74 if (part.length > 1 && part.startsWith('0')) return false; 75 } 76 77 return true; 78} 79 80 81/** 82 * íì ì ë ¥ê°ì´ í¬í¨ëì´ ìëì§ íì¸íë í¨ì 83 * @param {Object} valuesObj - ì¬ì©ìê° ì ë ¥í ê° ê°ì²´ (key-value ì) 84 * @param {Array} requiredList - íì ì ë ¥ê° ëª©ë¡ (í¤, ì 목, ì íì ì ë³´ í¬í¨) 85 * @returns {boolean} - íìê°ì´ 모ë í¬í¨ëì´ ìì¼ë©´ `true`, ëë½ë ê°ì´ ìì¼ë©´ `false` 86 */ 87function checkRequired(valuesObj, requiredList) { 88 // íìê°ì´ í¬í¨ëì´ìëì§ íì¸ 89 for (let requiredObj of requiredList) { 90 const key = requiredObj.key; // íì ì ë ¥ê°ì í¤ 91 const title = requiredObj.title; // íì ì ë ¥ê°ì ì 목 (ë¼ë²¨) 92 const values = valuesObj[key]; // ì¤ì ì ë ¥ê° 93 94 // ì ë ¥ê°ì´ ë¹ì´ ìì¼ë©´ ê²½ê³ ì°½ì ëì°ê³ , í´ë¹ ììì í¬ì»¤ì¤ë¥¼ ë§ì¶ í `false` ë°í 95 if (isNull(values)) { 96 alert(title + getMessage("confirm.required")); 97 requiredObj.selector.focus(); 98 return false; 99 } 100 } 101 return true; // 모ë íìê°ì´ ì ë ¥ë ê²½ì° `true` ë°í 102} 103 104/** 105 * í¹ì 컨í ì´ë ë´ìì `required` íì ì ë ¥ê° ìì를 ì¶ì¶íë í¨ì 106 * @param {string} containerSelector - íì ì ë ¥ê°ì ì°¾ì 컨í ì´ëì CSS ì íì 107 * @returns {Array | null} - íì ì ë ¥ ìì ê°ì²´ ë°°ì´ (컨í ì´ëê° ìì¼ë©´ `null` ë°í) 108 */ 109function getRequired(containerSelector) { 110 const container = document.querySelector(containerSelector); 111 if (!container) return null; 112 return getRequiredContainer(container); 113} 114 115/** 116 * í¹ì 컨í ì´ë ë´ìì `required` íì ì ë ¥ê° ìì를 ì¶ì¶íë í¨ì 117 * @param {HTMLElement} container - í¼ ì ë ¥ íëê° í¬í¨ë 컨í ì´ë ìì 118 * @returns {Array} - íì ì ë ¥ ìì ì ë³´ ê°ì²´ ë°°ì´ 119 */ 120function getRequiredContainer(container) { 121 // 컨í ì´ë ë´ì 모ë ì ë ¥ ìì (`input`, `select`, `textarea`)를 ê°ì ¸ì´ 122 const elements = container.querySelectorAll("input, select, textarea"); 123 const values = []; 124
125 elements.forEach(element => { 126 let key = element.name || element.id; // `name`ì´ ìì¼ë©´ `id` ì¬ì© 127 if (!key) return; // `name`ê³¼ `id` ë ë¤ ìì¼ë©´ 무ì 128 if (!element.required) return; // íì ì ë ¥ê°ì´ ìë ììë ì ì¸ 129 130 // íì ì ë ¥ ìì ê°ì²´ ìì± 131 const obj = {}; 132 obj.key = key; // íì ì ë ¥ê°ì í¤ (í¼ ììì `name` ëë `id`) 133 obj.title = element.title; // íì ì ë ¥ê°ì ë¼ë²¨ (ì ë ¥ ììì `title` ìì±) 134 obj.required = element.required; // `required` ì¬ë¶ 135 obj.selector = element; // í´ë¹ ì ë ¥ ìì ê°ì²´ 136 values.push(obj); 137 }); 138 139 return values; // íì ì ë ¥ ìì ê°ì²´ ëª©ë¡ ë°í 140} 141 142 143/** 144 * 컨í ì´ë ìì ë´ì 모ë ì ë ¥ê°ì ê°ì ¸ì¤ë í¨ì 145 * @param {string} containerSelector - í¼ ììê° í¬í¨ë 컨í ì´ëì CSS ì íì 146 * @returns {Object | null} - 컨í ì´ë ë´ í¼ ììì ê°ì í¬í¨íë ê°ì²´ (컨í ì´ëê° ìì¼ë©´ `null` ë°í) 147 */ 148function getFormValues(containerSelector) { 149 // í¹ì div(container) ìì 모ë form ìì를 ê°ì ¸ì´ 150 const container = document.querySelector(containerSelector); 151 if (!container) return null; 152 return getFormValuesContainer(container); 153} 154 155/** 156 * 컨í ì´ë ìì ë´ì 모ë ì ë ¥ê°ì ê°ì ¸ì¤ë í¨ì 157 * @param {HTMLElement} container - í¼ ì ë ¥ íë를 í¬í¨íë 컨í ì´ë ìì 158 * @returns {Object} - ì ë ¥ê°ì í¤-ê° ìì¼ë¡ ë°ííë ê°ì²´ 159 */ 160function getFormValuesContainer(container) { 161 // 컨í ì´ë ë´ ëª¨ë input, select, textarea ìì를 ì í 162 const elements = container.querySelectorAll("input, select, textarea"); 163 const values = {}; 164 165 elements.forEach(element => { 166 let key = element.name || element.id; // nameì´ ìì¼ë©´ id ì¬ì© 167 if (!key) return; // nameê³¼ id ë ë¤ ìì¼ë©´ 무ì 168 169 const isMultiple = container.querySelectorAll(`[name="${element.name}"]`).length > 1; 170 171 if (element.type === "checkbox") { 172 if (isMultiple) { 173 if (!values[key]) values[key] = []; 174 if (element.checked) values[key].push(element.value); 175 } else { 176 if(element.checked){ 177 values[key] = element.value; 178 } 179 } 180 } else if (element.type === "radio") { 181 if (element.checked) { 182 values[key] = element.value; 183 } 184 } else { 185 if (isMultiple) { 186 if (!values[key]) values[key] = []; 187 values[key].push(element.value); 188 } else { 189 values[key] = element.value; 190 } 191 } 192 }); 193 194 return values; 195} 196 197 198/** 199 * 컨í ì´ë ìì ë´ì 모ë `<input type="file">`ìì íì¼ì ê°ì ¸ì¤ë í¨ì 200 * @param {string} containerSelector - íì¼ ì ë ¥ íëê° í¬í¨ë 컨í ì´ëì CSS ì íì 201 * @returns {File[] | null} - 컨í ì´ë ë´ ëª¨ë íì¼ ì ë ¥ íëìì ì íë íì¼ ëª©ë¡ (íì¼ì´ ìì¼ë©´ `null` ë°í) 202 */ 203function getFormFiles(containerSelector) { 204 const container = document.querySelector(containerSelector); 205 if (!container) return null; 206 return getFormFilesContainer(container); 207} 208 209/** 210 * 컨í ì´ë ìì ë´ì 모ë `<input type="file">`ìì íì¼ì ê°ì ¸ì¤ë í¨ì 211 * @param {HTMLElement} container - íì¼ ì ë ¥ íë를 í¬í¨íë 컨í ì´ë ìì 212 * @returns {File[]} - ì íë íì¼ ê°ì²´ ë°°ì´ (íì¼ì´ ìì¼ë©´ ë¹ ë°°ì´ ë°í) 213 */ 214function getFormFilesContainer(container) { 215 // 컨í ì´ë ë´ì 모ë íì¼ ì ë ¥ íë ê°ì ¸ì¤ê¸° 216 const inputFileList = container.querySelectorAll('input[type="file"]'); 217 const fileList = []; 218
219 // ê° íì¼ ì ë ¥ íëìì 첫 ë²ì§¸ íì¼ì ê°ì ¸ì ë°°ì´ì ì¶ê° 220 for (let fileBox of inputFileList) { 221 const file = fileBox.files[0]; 222 if (file) fileList.push(file); 223 } 224 225 return fileList; 226} 227 228 229/** 230 * JS ê°ì²´ ë°ì´í°ë¥¼ í¹ì div ë´ form ììì ëì ì¼ë¡ ì ë ¥íë í¨ì 231 * @param {Object} data - ì ë ¥í ë°ì´í° ê°ì²´ (key: ììì name ìì±) 232 * @param {string} containerSelector - ì ë ¥í í¼ì´ í¬í¨ë divì ì íì 233 */ 234function populateFormValues(data, containerSelector) { 235 const container = document.querySelector(containerSelector); 236 if (!container) return; 237 238 resetFormValues(containerSelector); // ê° ì´ê¸°í 239 240 // container ë´ë¶ì 모ë input, select, textarea ìì ì í 241 const elements = container.querySelectorAll("input, select, textarea"); 242 243 elements.forEach(element => { 244 const name = element.name; 245 if (!name || !(name in data)) return; // nameì´ ìê±°ë dataì í´ë¹ keyê° ìì¼ë©´ ì¤íµ 246 247 const value = data[name]; 248 249 switch (element.type) { 250 case "radio": 251 // nameì´ ëì¼í ë¼ëì¤ ë²í¼ ì¤, valueê° ì¼ì¹íë ê² ì²´í¬ 252 const radioGroup = container.querySelectorAll(`input[type="radio"][name="${name}"]`); 253 radioGroup.forEach(radio => { 254 radio.checked = radio.value === String(value); 255 }); 256 break; 257 case "checkbox": 258 // ì²´í¬ë°ì¤ë ë°°ì´ì¼ ê²½ì° ì¬ë¬ ê° ì²´í¬ ê°ë¥ 259 if (Array.isArray(value)) { 260 element.checked = value.includes(element.value); 261 } else { 262 element.checked = Boolean(value); 263 } 264 break; 265 case "select-one": 266 // select ìì ê° ì¤ì 267 element.value = value; 268 break; 269 default: 270 // ì¼ë° input, textarea ê° ì¤ì 271 element.value = value; 272 break; 273 } 274 }); 275} 276 277/** 278 * í¹ì 컨í ì´ë ë´ì 모ë ì ë ¥ ìì ê°ì ì´ê¸°ííë í¨ì 279 * @param {string} containerSelector - ê°ì ì´ê¸°íí 컨í ì´ëì CSS ì íì 280 */ 281function resetFormValues(containerSelector) { 282 const container = document.querySelector(containerSelector); 283 if (!container) return; 284 285 // 컨í ì´ë ë´ì 모ë input, select, textarea ìì ê°ì ¸ì¤ê¸° 286 const elements = container.querySelectorAll("input, select, textarea"); 287 288 // ë°ë³µë¬¸ì¼ë¡ ê° ì´ê¸°í 289 elements.forEach(element => { 290 if ((element.tagName === "INPUT") && (element.type === "checkbox" || element.type === "radio")) { 291 element.checked = false; // ì²´í¬ë°ì¤ì radioë²í¼ì ì²´í¬ë¥¼ í´ì íë¤. 292 } else if(element.tagName === "INPUT" || element.tagName === "TEXTAREA"){ 293 element.value = ""; //ê° ì´ê¸°í 294 } else if (element.tagName === "SELECT") { 295 element.selectedIndex = 0; // 첫 ë²ì§¸ ìµì ì í 296 } 297 }); 298} 299 300 301/** 302 * ëì ì¼ë¡ HTML formì ìì±íê³ í¹ì URLë¡ ë°ì´í°ë¥¼ ì ì¶íë í¨ì 303 * 304 * @param {string} url - ë°ì´í°ë¥¼ ì ì¡í ëì URL 305 * @param {Object} options - ìµì ê°ì²´ 306 * @param {string} [options.method="GET"] - HTTP ìì² ë©ìë ("GET" ëë "POST" ë±) 307 * @param {Object} [options.data={}] - í¼ ë°ì´í° ê°ì²´ (í¤-ê° ì) 308 * @param {boolean} [options.viewLoading=true] - ë¡ë© íë©´ íì ì¬ë¶ 309 */ 310function submitForm(url, { method = "GET", data = {}, target = "_self", enctype = 'application/x-www-form-urlencoded', viewLoading = true} = {}) { 311 if(viewLoading) showLoading(); // ë¡ë© íë©´ì íìí ì§ ì¬ë¶ íì¸ í ì¤í 312 313 method = method.toUpperCase(); // HTTP ë©ìë를 ë문ìë¡ ë³í (ì: "post" -> "POST") 314 315 // 1. form ìì ìì± ë° ê¸°ë³¸ ì¤ì 316 const form = document.createElement('form'); 317 form.method = method; // HTTP ë©ìë ì¤ì (GET ëë POST) 318 form.action = url; // ìì²ì ë³´ë¼ URL ì§ì 319 form.target = target; // target ìµì 320 form.enctype = enctype; // enctype ìµì 321 322 // 2. data ê°ì²´ë¥¼ 기ë°ì¼ë¡ input ìì를 ìì±íì¬ formì ì¶ê° 323 if (!isEmptyObject(data) && Object.keys(data).length > 0 && typeof data === 'object') { 324 for (const key in data) { 325 const input = document.createElement('input'); 326 input.type = 'hidden'; // íë©´ì ë³´ì´ì§ ìë hidden input 327 input.name = key; // inputì name ìì± ì¤ì 328 input.value = data[key];
328 // inputì ê° ì¤ì 329 form.appendChild(input); // formì ì¶ê° 330 } 331 } 332 333 // 3. formì bodyì ì¶ê°í í ìë ì ì¶ 334 document.body.appendChild(form); 335 form.submit(); 336} 337 338 339// ì주ì¬ì©íë fetch기ë¥ì 묶ì´ì í í릿íí function 340/** 341 * HTTP ìì²ì ìííë `fetch` ëí¼ í¨ì 342 * 343 * @param {string} url - ìì²ì ë³´ë¼ URL 344 * @param {Object} [data={}] - ì ì¡í ë°ì´í° (GET ìì²ì ê²½ì° ì¿¼ë¦¬ ì¤í¸ë§ì¼ë¡ ë³íë¨) 345 * @param {Object} [options={}] - ì¶ê° ìµì ê°ì²´ 346 * @param {string} [options.method="POST"] - HTTP ìì² ë©ìë ("GET", "POST", "PUT", "DELETE" ë±) 347 * @param {Object} [options.headers={}] - ì¶ê° ìì² í¤ë (기본ì ì¼ë¡ JSON í¤ë í¬í¨) 348 * @param {AbortSignal} [options.signal=null] - ìì² ì·¨ì를 ìí AbortController ì í¸ 349 * @param {boolean} [options.isShowLoading=true] - ìì² ì¤ ë¡ë© UI íì ì¬ë¶ 350 * @returns {Promise<Object|string>} - ìëµ ë°ì´í°ë¥¼ JSON ëë í ì¤í¸ ííë¡ ë°í 351 */ 352async function fetchWrapper(url, data = {}, { method = "POST", headers = {}, signal = null, isShowLoading = true } = {}) { 353 method = method.toUpperCase(); // HTTP ë©ìë를 ë문ìë¡ ë³í ("post" -> "POST") 354 355 // 기본 í¤ë ì ì (JSON ìì²ì 기본ì¼ë¡ ì¤ì ) 356 const defaultHeaders = { 357 "Content-Type": "application/json", 358 "Accept": "application/json", 359 }; 360 361 // ì¬ì©ì ì ì í¤ëì 기본 í¤ë ë³í© 362 const mergedHeaders = Object.assign({}, defaultHeaders, headers); 363 //console.log(mergedHeaders); 364 365 // signal ê¸°ë³¸ê° ì¸í 366 if(signal === null){ 367 const controller = new AbortController(); // ê¸°ë³¸ê° 368 signal = controller.signal; 369 370 // ìë¡ê³ 침/í ë«ê¸°/íì´ì§ ì´ë ì ìì² ì¤ë¨ 371 window.addEventListener("beforeunload", () => { 372 console.log("beforeunload íì´ì§ ë ë¨ ê°ì§ - > controller.abort()"); 373 controller.abort("íì´ì§ ë ë¨"); 374 }); 375 } 376 377 // ìì² ìµì ê°ì²´ êµ¬ì± 378 const options = { 379 method, 380 headers: mergedHeaders, 381 signal : signal, 382 }; 383 384 /* 385 * mode, cache, credentials, redirect, referrerPolicyë 기본ê°ì ì¬ì© 386 * 387 * 기본ê°: 388 * mode: "cors" 389 * cache: "default" 390 * credentials: "same-origin" 391 * redirect: "follow" 392 * referrerPolicy: "strict-origin-when-cross-origin" 393 */ 394 395 // GET ìì²ì¼ ê²½ì°, data를 쿼리ì¤í¸ë§ì¼ë¡ ë³ííì¬ URLì ì¶ê° 396 if (method === "GET" && Object.keys(data).length > 0) { 397 url += `?${new URLSearchParams(data).toString()}`; 398 } 399 // GETì´ ìë ê²½ì°, ìì² bodyì JSON ë°ì´í°ë¥¼ í¬í¨ 400 else if (method !== "GET" && data !== null && Object.keys(data).length > 0) { 401 options.body = JSON.stringify(data); 402 } 403 404 try { 405 //console.log(`ð Fetching: ${url} [${method}]`); // ìì² ì ë³´ ì½ìì ì¶ë ¥ 406 407 if (isShowLoading) showLoading(); // ìì² ì¤ ë¡ë© íì 408 const response = await fetch(url, options); // fetch APIë¡ ìì² ì¤í 409 if (isShowLoading) showLoading(false); // ìì² ì¢ ë£ í ë¡ë© ì ê±° 410 411 // ì¸ì ì´ í´ì ëìì ê²½ì° (401 Unauthorized) 412 if (!response.ok && response.status === 401) { 413 const contentType = response.headers.get("content-type"); 414 415 // ìëµì´ JSON íìì´ë©´, ë©ìì§ë¥¼ ê°ì ¸ì ê²½ê³ ì°½ íì í ë¡ê·¸ì¸ íì´ì§ë¡ ì´ë 416 if (contentType && contentType.includes("application/json")) { 417 const errorJson = await response.json(); 418 alert(errorJson.message); 419 location.href = '/'; // ë¡ê·¸ì¸ íì´ì§ë¡ ì´ë 420 return; 421 } 422 423 // 기본ì ì¸ ìë¬ ì²ë¦¬ 424 alert("ìë¬ê° ë°ìíìµëë¤."); 425 location.href = '/'; 426 return; 427 } 428 429 // ìëµ Content-Type íì¸ í JSON ëë í ì¤í¸ë¡ ë³í 430 const contentType = response.headers.get("content-type"); 431 if (contentType && contentType.includes("application/json")) { 432 return await response.json(); // JSON ìëµ ë°í 433 } else { 434 return await response.text(); // í ì¤í¸ ìëµ ë°í 435 } 436 437 } catch (error) { 438 if (isShowLoading) showLoading(false); 439 let message = error.message; 440 441 console.error("Fetch error:", error); 442 console.error("fetchWrapper - error - error.message : ", error.message); 443 console.error("fetchWrapper - error - error.name : ", error.name); 444 console.error("fetchWrapper - error - isUnloading : ", isUnloading); 445 446 if (isUnloading) { 447 message = "íì´ì§ë¥¼ ë²ì´ëë©´ì ìì²ì´ ì·¨ìëììµëë¤."; 448 } else if(error.name === 'AbortError'){ 449 message = "ìì²ì´ ì¤ë¨ëììµëë¤." 450 } else if (message === "Failed to fetch") { 451 message = "ìë² ìëµ ì¤í¨. ë¤í¸ìí¬ ìí를 íì¸í´ì£¼ì¸ì."; 452 //return { status: "error"}; 453 } 454 455 return { status: "suceess", message: "" }; // ì¤ë¥ ë°ì ì ìë¬ ê°ì²´ ë°í 456 } 457} 458 459/* 460// signal ì¬ì©ë°©ë² 461const controller = new AbortController(); 462const signal = controller.signal; 463 464setTimeout(() => controller.abort(), 1000);
464 // 1ì´ í ìì² ì·¨ì 465 466fetchWrapper("/api/search", { params: { query: "Alice" }, signal }) 467 .then(data => console.log("ê²ì ê²°ê³¼:", data)) 468 .catch(error => console.error("ìì² ì·¨ìë¨:", error.message)); 469*/ 470 471/** 472 * ìë²ì íì¼ì ì ë¡ëíë í¨ì 473 * 474 * @param {File[]} fileList - ì ë¡ëí íì¼ ë°°ì´ 475 * @param {string | null} [fileId=null] - íì¼ì ìë³í ID (ì íì ) 476 * @returns {Promise<Object | null>} - ì ë¡ëë íì¼ ì ë³´ (ì±ê³µ ì), ì¤í¨ ì `null` 477 */ 478async function fileUpload(fileList, fileId = null) { 479 480 const formData = new FormData(); 481 482 // íì¼ ëª©ë¡ì FormDataì ì¶ê° 483 for (const file of fileList) { 484 formData.append('fileList', file); 485 } 486 487 // fileIdê° ì¡´ì¬íë©´ FormDataì ì¶ê° 488 if (!isNull(fileId)) formData.append("fileId", fileId); 489 490 const options = { 491 method: "POST", 492 body: formData, 493 headers: { 494 "Accept": "application/json", 495 } 496 }; 497 498 // ìë²ì íì¼ ì ë¡ë ìì² 499 const response = await fetch("/files/upload", options); 500 501 // ìì²ì´ ì¤í¨í ê²½ì° null ë°í 502 if (!response.ok) return null; 503 504 const data = await response.json(); 505 506 // ì ë¡ë ê²°ê³¼ê° ì¤í¨í ê²½ì° null ë°í 507 if (!data.success) return null; 508 509 return data.data; // ì ë¡ëë íì¼ ë°ì´í° ë°í 510} 511 512/** 513 * ë¡ë© íë©´ì íì ëë ì¨ê¸°ë í¨ì 514 * @param {boolean} [bool=true] - `true`ì´ë©´ ë¡ë© íë©´ íì, `false`ì´ë©´ ì¨ê¹ 515 */ 516function showLoading(bool = true) { 517 const loadingOverlay = document.querySelector("#loadingOverlay"); 518 if (loadingOverlay) { 519 loadingOverlay.style.display = bool ? "block" : "none"; 520 } 521} 522 523/** 524 * í¹ì HTML ìì를 ì¨ê¸°ë í¨ì 525 * @param {HTMLElement} el - ì¨ê¸¸ ëì ìì 526 */ 527function hideElement(el) { 528 el.style.display = "none"; 529} 530 531/** 532 * í¹ì HTML ìì를 íìíë í¨ì 533 * @param {HTMLElement} el - íìí ëì ìì 534 */ 535function showElement(el) { 536 el.style.display = "block"; 537} 538 539/** 540 * inputíê·¸ì fileì validateíë í¨ì 541 * @param inputElement - input elìì 542 * @param isCheckImage - checkí ê²ì´ ì´ë¯¸ì§ì¸ì§ ìëì§ ì¬ë¶ 543 */ 544function validateFile(inputElement, isCheckImage = false) { 545 const file = inputElement.files[0]; 546 if (!file) return; 547 548 // íì©ë íì¥ì & MIME íì 549 const allowedExtensions = fileProperties.allowExtensions; 550 const allowExtensionsImages = fileProperties.allowExtensionsImages; 551 const allowedMimeTypes = fileProperties.allowMimeTypes; 552 /* 553 // íì©ë íì¥ì & MIME íì 554 const allowedExtensions = [".doc", ".docx", ".pdf", ".txt", ".hwp", ".hwpx", 555 ".xls", ".xlsx", ".csv", 556 ".gif", ".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tiff"]; 557 558 const allowedMimeTypes = ["application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 559 "application/pdf", "text/plain", "application/x-hwp", 560 "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 561 "image/gif", "image/jpeg", "image/png", "image/bmp", "image/webp", "image/tiff", "text/csv"]; 562 // MIME íì ê²ì¬, MIME ê²ì¬ë backendìì 563 const isValidMimeType = allowedMimeTypes.includes(file.type); 564 * */ 565 566 // íì¥ì ê²ì¬ 567 const fileName = file.name.toLowerCase(); 568 569 // ì´ë¯¸ì§ë§ ì²´í¬íëì§ ì¬ë¶ì ë°ë¼ íì©íì¥ìê° ë¬ë¼ì§. 570 const isValidExtension = isCheckImage ? allowExtensionsImages.some(ext => fileName.endsWith(ext)) : allowedExtensions.some(ext => fileName.endsWith(ext)); 571 572 // MIME íì ê²ì¬ 573 if (!isValidExtension) { 574 alert('íì©ëì§ ìì íì¼ íìì ëë¤!'); 575 inputElement.value = ""; // ì ë ¥ íë ì´ê¸°í 576 } 577 return isValidExtension; 578} 579 580 581/** 582 * iframeì resizeíë í¨ì 583 * @param iframe - iframe ê°ì²´ 584 * @param {Object} [options={}] - ì¶ê° ìµì ê°ì²´ 585 */ 586function resizeIframe(iframe, { minHeight = 400, maxHeight = 1600 }={}) { 587 588 if (!iframe.contentWindow || !iframe.contentWindow.document.body) return; 589 590 const scrollHeight = iframe.contentWindow.document.body.scrollHeight + 100; 591 const adjustedHeight = Math.max(minHeight, Math.min(scrollHeight, maxHeight)); 592 iframe.style.height = adjustedHeight + 'px'; 593} 594 595function getMobileOS() { 596 const userAgent = navigator.userAgent || navigator.vendor || window.opera; 597 598 if (/android/i.test(userAgent)) { 599 return "Android"; 600 } 601 602 if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { 603 return "iOS"; 604 } 605 606 return "unknown"; 607} 608 609 610/** 611 * í¹ì ì¡°ê±´ì´ trueê° ë ëê¹ì§ 기ë¤ë¦°ë¤. 612 */ 613function waitForTrue(getFlagFn, checkInterval = 100, timeout = 5000) { 614 return new Promise((resolve, reject) => { 615 const timer = setInterval(() => { 616 if (getFlagFn()) { 617 clearInterval(timer); 618 clearTimeout(timeoutTimer); 619 resolve(); 620 } 621 }, checkInterval); 622 623 const timeoutTimer = setTimeout(() => { 624 clearInterval(timer); 625 reject(new Error('Timeout: ì¡°ê±´ì´ trueê° ëì§ ìììµëë¤.')); 626 }, timeout); 627 }); 628} 629/** 630 * like ê²ì ìì 모ì 631 */ 632function getChoseong(text) { 633 const CHO = ["ã±","ã²","ã´","ã·","ã¸","ã¹","ã ","ã ","ã ","ã ","ã ","ã ","ã ","ã ","ã ","ã ","ã ","ã ","ã "]; 634 let result = ""; 635 636 for (let i = 0; i < text.length; i++) { 637 const code = text.charCodeAt(i) - 44032; 638 if (code >= 0 && code <= 11171) { 639 const choIndex = Math.floor(code / 588); 640 result += CHO[choIndex]; 641 } else { 642 result += text[i]; // íê¸ ìë ê²½ì°ë ê·¸ëë¡ 643 } 644 } 645 return result; 646}
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.