1/** 2 * core/index.js 3 * 4 * íë¡ì í¸ ë´ìì ê³µíµì ì¼ë¡ ì¬ì©ëë ìë°ì¤í¬ë¦½í¸ ì½ë를 ìì±íë íì¼ 5 * 6 * (1) ê° íì´ì§ìì ì¬ì©ëë ê²ë¤ì /page/.. ì ìì±, 7 * (2) ê° ì¬ì´í¸ìì ê³µíµì ì¼ë¡ ì¬ì©ëë ê²ë¤ì /core/site.js or /core/admin.js ì ìì± 8 * (3) ê·¸ ì¸ ê³µíµì ì¼ë¡ ì¬ì©ëë ê²ë¤ì ì´ íì¼ì ìì± 9 * 10 * */ 11 12 13/** 14 * ê³µëì¸ì¦ì ë°ê¸ì(issuer) ì문ì êµë´ CA ëª ì¹ì¼ë¡ 매ííë¤. 15 * ë°ê¸ì 문ìì´ì ìë³ìê° í¬í¨ëë©´ í´ë¹ CA ëª ì¹ì¼ë¡, ì´ë ê²ìë ê±¸ë¦¬ì§ ìì¼ë©´ ì문 ê·¸ëë¡ ë°ííë¤. 16 * 매í ì¶ê°/ìì ì ìë CERT_ISSUER_MAPë§ ê³ ì¹ë©´ ëë¤. (ê´ë¦¬ì/ì¬ì©ì ê³µíµ) 17 */ 18const CERT_ISSUER_MAP = { 19 yessign: 'ê¸ìµê²°ì ì', 20 kica: 'íêµì ë³´ì¸ì¦', 21 crosscert: 'íêµì ìì¸ì¦', 22 ktnet: 'íêµë¬´ìì ë³´íµì ', 23}; 24function mapCertIssuer(raw) { 25 if (raw == null) return raw; 26 const text = String(raw).trim(); 27 const lower = text.toLowerCase(); 28 const key = Object.keys(CERT_ISSUER_MAP).find(k => lower.includes(k)); 29 return key ? CERT_ISSUER_MAP[key] : text; 30} 31 32 33document.addEventListener('DOMContentLoaded', function () { 34 35 // íì´ì§ ë´ ê³µëì¸ì¦ì ë°ê¸ì ìì(.cert-issuer)를 CA ëª ì¹ì¼ë¡ ìë ì¹í (ì¬ì©ì ë±ë¡ íë©´ ë±) 36 document.querySelectorAll('.cert-issuer').forEach(function (el) { 37 el.textContent = mapCertIssuer(el.textContent); 38 }); 39 40 if (document.querySelector('#dashboardContainer') || document.querySelector('#homeContainer') || document.querySelector('#signUpCompleteContainer')) { 41 requestFcmPermission(); 42 } 43 44 // íì´ì§ ë¡ë© ì ì¤í¬ë¡¤ ìì¹ ì´ë 45 const urlParams = new URLSearchParams(window.location.search); 46 const scrollToValue = urlParams.get('goto'); 47 if (scrollToValue) { 48 // ì«ìì¼ ê²½ì° í½ì ë¨ìë¡ ì¤í¬ë¡¤ 49 if (!isNaN(scrollToValue)) { 50 scrollByPosition(scrollToValue); 51 } else { 52 // ID를 ê°ì§ ììê° ì¡´ì¬íë©´ í´ë¹ ìì¹ë¡ ì¤í¬ë¡¤ (ì¬ì ê³µê° ì¶ê°) 53 scrollById(scrollToValue); 54 } 55 56 // ìë£ëë©´ íì¬ URLìì gotoë§ ì ê±° 57 let params = getUrlParams(); 58 params.goto = undefined; 59 setUrlParams(params); 60 } 61 62 document.addEventListener('click', function (e) { 63 const scrollEl = e.target.closest('.scroll-to'); 64 if (scrollEl) { 65 // ì¸í°ëí°ë¸ ìì í´ë¦ ì scroll-to ëì 무ì 66 if (e.target.closest('a, button, input, select, textarea')) return; 67 68 const target = scrollEl.dataset.target; 69 const href = scrollEl.dataset.href; 70 71 if (target && href) { 72 // ë ë¤ ìì¼ë©´: ê°ì íì´ì§ì´ë©´ ì¤í¬ë¡¤, ë¤ë¥´ë©´ íì´ì§ ì´ë 73 // endsWithë¡ ë¹êµí´ ìë¸ëë©ì¸/ê²½ë¡ í리í½ì¤ íê²½ 모ë ëì 74 const targetUrl = new URL(href, location.origin); 75 if (location.pathname === targetUrl.pathname || location.pathname.endsWith(targetUrl.pathname)) { 76 scrollById(target); 77 } else { 78 location.href = href; 79 } 80 } else if (target) { 81 // data-targetë§: íì ì¤í¬ë¡¤ 82 scrollById(target); 83 } else if (href) { 84 // data-hrefë§: íì íì´ì§ ì´ë 85 location.href = href; 86 } 87 } 88 }); 89 90 // 3rd party name ìì± 91 document.querySelectorAll('.generate-3party-name').forEach((el) => { 92 generate3rdPartyName(el); 93 }); 94 95 // qna ì í í ì¤í¸ë¡ ë³í 96 document.querySelectorAll('.generate-qna-type').forEach((el) => { 97 generateQnaType(el); 98 }); 99 100 // event ì í í ì¤í¸ë¡ ë³í 101 document.querySelectorAll('.generate-event-type').forEach((el) => { 102 generateEventType(el); 103 }); 104 105 document.querySelectorAll('.generate-card-company-name').forEach((el) => { 106 let cardCode = el.dataset.cardCode; 107 let name = getKisCardCompanyName(cardCode); 108 el.textContent = name; 109 }); 110 111 document.querySelectorAll('.generate-risk-shop-type').forEach((el) => { 112 let otherCount = 0; // ~ì¸ nê±´ 113 if (el.dataset.count != null) { 114 otherCount = numberWithCommas((el.dataset.count - 1)); 115 } 116 117 let resStr = ''; 118 if (otherCount > 0) { 119 resStr = `${getRiskShopTypeStr(el.dataset.value)} ì¸ ${otherCount}ê±´`; 120 } else { 121 resStr = getRiskShopTypeStr(el.dataset.value); 122 } 123 124 el.textContent = resStr; 125 }); 126 127 // ëª©ë¡ í ì´ë¸ ì ë ¬ í¤ë 128 initSortHeader(); 129 130}); 131 132function scrollByPosition(scrollToValue) { 133 window.scrollTo({ 134 top: parseInt(scrollToValue, 10), 135 behavior: "smooth" 136 }); 137} 138 139function scrollById(scrollToValue) { 140 // ID를 ê°ì§ ììê° ì¡´ì¬íë©´ í´ë¹ ìì¹ë¡ ì¤í¬ë¡¤ (ì¬ì ê³µê° ì¶ê°) 141 const targetElement = document.getElementById(scrollToValue); 142 if (targetElement) { 143 const offset = 100; // ì쪽 ì¬ë°± 144 const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - offset; 145 146 window.scrollTo({ 147 top: targetPosition, 148 behavior: "smooth" 149 }); 150 } 151} 152 153// 3rd party name ìì± 154function generate3rdPartyName(el) { 155 if (!el) return; 156
157 waitForI18nInitialization(() => { 158 let deliveryCompany = el.dataset.deliveryCompany; 159 let cardCode = el.dataset.cardCode; 160 let key = deliveryCompany && deliveryCompany != '0' ? deliveryCompany : (cardCode ? cardCode : '0'); 161 162 let name = get3rdPartyName(key); 163 164 el.innerHTML = name; 165 }); 166} 167 168function get3rdPartyName(key) { 169 let name = SALES_CORP[key]?.name; 170 171 if (name == null || name == undefined) { 172 name = i18n.t('etc'); 173 } 174 175 return name; 176} 177 178// ì´ë²¤í¸ ì¢ ë¥ ì´ë¦ì¼ë¡ ë³í 179async function generateEventType(el) { 180 if (!el) return; 181 182 waitForI18nInitialization(() => { 183 let type = el.dataset.type; 184 let name = getEventTypeName(type); 185 el.innerHTML = name; 186 }); 187} 188 189// ì´ë²¤í¸ ì¢ ë¥ ì´ë¦ ê°ì ¸ì¤ê¸° 190function getEventTypeName(type) { 191 let name = '기í'; 192 193 switch (type) { 194 case 'early_signup_reward_ssonda_fee': 195 name = '첫 íìê°ì ììë£ ë¬´ë£'; 196 break; 197 case 'ssonda_fee_discount_in_period': 198 name = 'ìë¹ì¤ ì´ì©ë£ ê¸°ê° í ì¸'; 199 break; 200 case 'early_signup_reward_auto_reply': 201 name = '첫 íìê°ì ë°ë¡ëê¸ ë¬´ë£'; 202 break; 203 case 'auto_reply_discount': 204 name = 'ë°ë¡ëê¸ í ì¸'; 205 break; 206 case 'prepay_user': 207 name = 'ì ì ì°ê³ ê° í ì¸'; 208 break; 209 case 'lotto': 210 name = 'ë³µê¶'; 211 break;
212 case 'smallbiz_support': 213 name = 'ììê³µì¸ì§ì'; // legacy: DBì ë¨ììë 기존 ë°ì´í° íìì© 214 break; 215 case 'smallbiz_support_auto_reply': 216 name = 'ììê³µì¸ì§ì Aië°ë¡ëê¸'; 217 break; 218 case 'smallbiz_support_sales_ledger': 219 name = 'ììê³µì¸ì§ì ë°ë¡ì¥ë¶'; 220 break; 221 default: 222 name = '기í'; 223 break; 224 } 225 226 return name; 227} 228 229async function generateQnaType(el) { 230 if (!el) return; 231 232 waitForI18nInitialization(() => { 233 let type = el.dataset.type; 234 let name = getQnaTypeStr(type); 235 el.innerHTML = name; 236 }); 237} 238 239function getQnaTypeStr(type) { 240 switch (type) { 241 case '0': return i18n.t('qna.type.error'); 242 case '1': return i18n.t('qna.type.use'); 243 case '2': return i18n.t('qna.type.transaction'); 244 case '3': return i18n.t('qna.type.serviceStop'); 245 case '9': return i18n.t('qna.type.etc'); 246 default: return i18n.t('qna.type.etc'); 247 } 248} 249 250function getRiskShopTypeStr(type) { 251 switch (type) { 252 case 'surge_sales': return 'ë§¤ì¶ ê¸ì¦'; 253 case 'drop_sales': return 'ë§¤ì¶ ê¸ê°'; 254 case 'low_sales': return 'ì¥ê¸° 매ì¶ì ì¡°'; 255 case 'mass_cancel': return 'ë¤ì 매ì¶ì·¨ì'; 256 case 'change_adj_account': return 'ì ì°ê³ì¢ ë³ê²½'; 257 case 'change_sales_account': return '매ì¶ê³ì ì°ëì¤ë¥'; 258 case 'recovery_decrease': return 'ìíì¡ ê°ì'; 259 case 'remain_loan_surge': return '미ìíì¡ ê¸ì¦(íëì¡)'; 260 case 'remain_loan_week_surge': return '미ìíì¡ ê¸ì¦(ì 주)'; 261 case 'remain_loan_avg_surge': return '미ìíì¡ ê¸ì¦(ì 주 íê· )'; 262 case 'change_open_status': return 'ìì ìí ë³ê²½'; 263 case 'no_shop_info': return 'ì ì í´ì§'; 264 case 'seizure': return 'ì±ê¶ìë¥'; 265 case 'etc': return '기í'; 266 default: return '-'; 267 } 268} 269 270function getKisCardCompanyName(cardCode) { 271 let name = '기í'; 272 273 let cardCorps = PG_CARD_CORP; 274 275 if (cardCorps[cardCode]) { 276 name = cardCorps[cardCode].name; 277 } 278 279 return name; 280} 281 282/** 283 * ApexCharts ë¤ì¤ ì리ì¦(2ê° ì´ì) ê³µíµ í´í(custom) ìì±ê¸°. 284 * 기본 ë¤í¬ ë°ì¤ ë´ë¶ í ê°ê²©ì ì¢ê² ë ëíë¤. shared:true ì°¨í¸ìì ì¬ì©. 285 * 286 * @param {Object} [opts] 287 * @param {function(any, object):string} [opts.title] - íì´í í¬ë§· (label, w) => string. 기본: ì¹´í ê³ ë¦¬ ë¼ë²¨ ê·¸ëë¡ 288 * @param {function(number):string} [opts.value] - ê° í¬ë§·. 기본: numberWithCommas(v) + 'ì' 289 * @param {number} [opts.minWidth=120] - í´í ìµì ëë¹(px). ë´ì©ì´ ì§§ìë ì´ ëë¹ ì´ì ì ì§ 290 * @returns {function} ApexCharts tooltip.custom í¨ì 291 */ 292function buildSharedChartTooltip(opts = {}) { 293 const titleFn = opts.title || ((label) => label); 294 const valueFn = opts.value || ((v) => numberWithCommas(v) + 'ì'); 295 // reverse: í ìì ë¤ì§ê¸° â ì¤í ì°¨í¸ìì ìê°ì ìë¨ series를 í´í ì쪽ì íì 296 const reverse = opts.reverse === true; 297 const minWidth = opts.minWidth ?? 120; 298 const minWidthStyle = minWidth ? ` style="min-width:${minWidth}px;"` : ''; 299 return function ({ series, dataPointIndex, w }) { 300 const title = titleFn(w.globals.labels[dataPointIndex], w, dataPointIndex); 301 // íì 기본 í´í í´ëì¤ ì¬ì¬ì© â ë¼ë²¨/ê°(êµµê²) ì¤íì¼ ëì¼. ê°ê²© ì ë° y-group/series-group ëí¼ë 미ì¬ì©. 302 const order = w.globals.seriesNames.map((_, i) => i); 303 if (reverse) order.reverse(); 304 const rows = order.map((i) => { 305 const name = w.globals.seriesNames[i]; 306 const color = w.globals.colors[i]; 307 const val = series[i][dataPointIndex]; 308 return `<div class="d-flex align-items-center lh-1"> 309 <span class="d-inline-block rounded-circle me-1" style="width:10px;height:10px;background:${color};"></span> 310 <span class="apexcharts-tooltip-text-y-label">${name}:</span>
311 <span class="apexcharts-tooltip-text-y-value">${valueFn(val)}</span></div>`; 312 }).join(''); 313 // íì´í ë°ë 기본 í´ëì¤(apexcharts-tooltip-title)ë¡ ì본과 ëì¼. í ê°ê²©ë§ gap-3(12px)ë¡ ì ì´. 314 // minWidth ì§ì ì ë£¨í¸ ëí¼ì min-width â ì§§ì ë´ì©ìë ìµì ëë¹ ì ì§. 315 return `<div${minWidthStyle}><div class="apexcharts-tooltip-title">${title}</div><div class="d-flex flex-column gap-3 px-3 pt-2 pb-3">${rows}</div></div>`; 316 }; 317} 318 319/** 320 * [ê°ë°ì ë구] íë¡ì í¸ ê¸°ë³¸ ìì ë·°ì´ 321 * ì½ììì showProjectAssets() ë¡ í¸ì¶ 322 */ 323window.showProjectAssets = function () { 324 // íì¬ íì´ì§ DOMìì íë¹ì½Â·OG ì´ë¯¸ì§ë¥¼ ëì ì¼ë¡ ì½ì´ì´ 325 const faviconEl = document.querySelector('link[rel~="icon"]'); 326 const faviconSrc = faviconEl ? faviconEl.href : null; 327 const faviconLabel = faviconSrc ? faviconSrc.split('/').pop() : null; 328 329 const ogImageEl = document.querySelector('meta[property="og:image"]'); 330 const ogImageSrc = ogImageEl ? ogImageEl.content : null; 331 const ogImageLabel = ogImageSrc ? ogImageSrc.split('/').pop() : null; 332 333 const sections = [ 334 faviconSrc ? { 335 title: 'íë¹ì½ (íì¬ íì´ì§)', 336 items: [{ label: faviconLabel, src: faviconSrc }], 337 } : null, 338 { 339 title: 'ë¡ê³ ', 340 items: [ 341 { label: 'logo-brand.svg', src: '/assets/images/logo-brand.svg' }, 342 { label: 'logo-brand.png', src: '/assets/images/logo-brand.png' }, 343 { label: 'logo-brand-white.svg', src: '/assets/images/logo-brand-white.svg', dark: true }, 344 { label: 'logo-icon.svg', src: '/assets/images/logo-icon.svg' }, 345 { label: 'logo.svg', src: '/assets/images/logo.svg' }, 346 ], 347 }, 348 ogImageSrc ? { 349 title: 'OG ì´ë¯¸ì§ (íì¬ íì´ì§)', 350 items: [{ label: ogImageLabel, src: ogImageSrc }], 351 } : null, 352 ].filter(Boolean); 353 354 const renderItems = (items) => items.map(item => ` 355 <div class='d-inline-flex flex-column align-items-center gap-2 m-2'> 356 <a href='${item.src}' target='_blank' class='d-block p-4 border rounded-3 ${item.dark ? 'bg-dark' : 'bg-light'}' title='${item.src}'> 357 <img src='${item.src}' class='mh-120px mw-175px d-block object-fit-contain' onerror='this.closest("a").style.opacity="0.4"' /> 358 </a> 359 <span class='fs-6 text-gray-600 text-center' style='max-width:190px;word-break:break-all;'>${item.label}</span> 360 </div> 361 `).join(''); 362 363 const body = sections.map(sec => ` 364 <div class='mb-8'> 365 <div class='fs-5 fw-bold text-uppercase text-gray-500 border-bottom border-2 pb-3 mb-5'>${sec.title}</div> 366 <div class='d-flex flex-wrap gap-2'>${renderItems(sec.items)}</div> 367 </div> 368 `).join('') + '<p class=\'fs-7 text-muted mt-2\'>ì´ë¯¸ì§ë¥¼ í´ë¦íë©´ ì íìì ì본ì ë³¼ ì ììµëë¤.</p>'; 369 370 const modal = new DynamicModal('íë¡ì í¸ ê¸°ë³¸ ìì ', body); 371 modal.setModalSize('lg'); 372 modal.setOptions({ verticalCenter: true, scrollBody: true }); 373 modal.hideButton(); 374 modal.show(); 375}; 376 377/** 378 * ê°ì´ë í´í ì´ê¸°í (ê´ë¦¬ì, íí¸ëììë§ ì¬ì©) 379 * 380 * @desc [content]ê° ìì¼ë©´ ì°ì íìíê³ , ìì¼ë©´ [type]ì¼ë¡ i18nìì ë¶ë¬ì´ 381 */ 382function initGuideTooltip(container = document) { 383 384 waitForI18nInitialization(() => { 385 container.querySelectorAll('.guide-tooltip').forEach((el) => { 386 let content = el.dataset.tooltipContent; 387 let type = el.dataset.tooltipType; 388 389 // contentê° ë¹ì´ìê³ typeì´ ìì ê²½ì° i18nìì ë¶ë¬ì¤ê¸° 390 if (!isNullOrEmpty(type) && isNullOrEmpty(content)) { 391 content = i18n.t(`admin:tooltip.${type}`); 392 } 393 394 if (isNullOrEmpty(content)) { 395 return; 396 } 397 398 el.innerHTML = '<i class="fa-regular fa-circle-question"></i>' 399 const tooltip = new bootstrap.Tooltip(el, { 400 title: i18n.t(`admin:tooltip.${type}`), 401 placement: 'top', 402 // ì¬ë¬ ì¤ ë¬¸êµ¬ë data-tooltip-class="custom-tooltip"ë¡ ì¢ì¸¡ì ë ¬ ì§ì (기본ì ê°ì´ë°ì ë ¬) 403 customClass: el.dataset.tooltipClass || 'tooltip-inverse', 404 html: true, 405 }) 406 }); 407 }); 408} 409 410/** 411 * ìí¸í(secure) íì¼ ë©í ì¡°í + ì´ë¯¸ì§/PDF íë¨ 412 * 413 * - secure íì¼ì íì¼ëª ì´ ì«ì idë¿ì´ë¼ /info ìëµ
413ì mimeType·originalNameì¼ë¡ íë¨íë¤ 414 * - mimeì´ octet-stream ë±ì´ë©´(ì± ì ë¡ë contentType ëë½) originalName íì¥ìë¡ í´ë°± íë¨ 415 * - typeAttr: file-download/file-upload íê·¸ì ê·¸ëë¡ ë£ì ìì± ë¬¸ìì´ (is-image / is-pdf / ë¹ê°) 416 * 417 * usage: const meta = await getSecureFileMeta(filePath, fileName); 418 */ 419async function getSecureFileMeta(filePath, fileName) { 420 const meta = { isImage: false, isPdf: false, originalName: null, typeAttr: '' }; 421 if (!/^\d+$/.test(String(fileName))) { 422 return meta; 423 } 424 425 try { 426 const info = await fetch(`${filePath}/${fileName}/info`, { credentials: 'include' }).then((r) => (r.ok ? r.json() : null)); 427 const mime = info?.data?.mimeType ?? ''; 428 meta.originalName = info?.data?.originalName ?? null; 429 430 const orgExt = (meta.originalName ?? '').split('.').pop().toLowerCase(); 431 if (mime.indexOf('image/') === 0 || ['jpg', 'jpeg', 'jfif', 'png', 'gif', 'webp', 'bmp'].includes(orgExt)) { 432 meta.isImage = true; 433 } else if (mime === 'application/pdf' || orgExt === 'pdf') { 434 meta.isPdf = true; 435 } 436 } catch (e) { /* ì¡°í ì¤í¨ ì ê¸°ë³¸ê° (ë¤ì´ë¡ë ë²í¼ë§ íì) */ } 437 438 meta.typeAttr = meta.isImage ? 'is-image="true"' : meta.isPdf ? 'is-pdf="true"' : ''; 439 return meta; 440} 441 442/** 443 * ëª©ë¡ í ì´ë¸ ì ë ¬ í¤ë ì´ê¸°í 444 * 445 * @desc ì ë ¬í <th> ìì ìë í ì¤ë§ ì ì¸íë©´ íì´í 주ì /ìí íì/í ê¸ì´ ì ë¶ ì²ë¦¬ëë¤ 446 * <span class="sort-header cursor-pointer text-hover-primary d-inline-flex align-items-center" data-sort="컬ë¼í¤">í¤ëëª </span> 447 * 448 * - data-sort : ìë² ë§¤í¼ê° ë¶ê¸°í ì»¬ë¼ í¤ 449 * - data-order : íì± í¤ëìë§ ì´ í¨ìê° ë¶ì¬íë íì¬ ë°©í¥ (asc/desc) 450 * 451 * í¤ë를 ë르면 ë´ë¦¼ì°¨ì â ì¤ë¦ì°¨ì â í´ì 3ë¨ê³ë¥¼ ìííë¤. 452 * íì´í(â²â¼)ë ìí íìì©ì¼ë¡ íì ë³´ì´ë©°, íì± ë°©í¥ë§ ì§íê² ê°ì¡°íë¤. 453 * 454 * ì ë ¬ ê°ì form#searchForm ì hidden sort/order ë¡ ì ë¬íë¤. (ìì¼ë©´ ì¬ê¸°ì ë§ë ë¤) 455 * csv.jsê° searchForm ì 체를 FormDataë¡ ì½ì´ê°ë¯ë¡ ë¤ì´ë¡ëìë ê°ì ì ë ¬ì´ ì ì©ëë¤. 456 */ 457function initSortHeader() { 458 459 const ARROW_HTML = ` 460 <span class="d-inline-flex flex-column lh-1 ms-2 fs-10 text-gray-700" aria-hidden="true"> 461 <span class="sort-arrow" data-order="asc">▲</span> 462 <span class="sort-arrow" data-order="desc">▼</span> 463 </span> 464 `; 465 466 let headers = document.querySelectorAll('.sort-header'); 467 let searchForm = document.querySelector('form#searchForm'); 468 469 if (headers.length == 0 || !searchForm) return;
469 // ì ë ¬ í¤ëê° ìë íë©´ì ì무ê²ë íì§ ìëë¤ 470 471 // íì¬ ì ë ¬ ìíë URLì´ ê°ê³ ìë¤ (ì´ íë©´ì 그린 ì¡°ê±´ ê·¸ëë¡) 472 let params = new URLSearchParams(location.search); 473 let sortInput = ensureHiddenInput('sort', params.get('sort')); 474 let orderInput = ensureHiddenInput('order', params.get('order')); 475 476 headers.forEach((header) => { 477 header.insertAdjacentHTML('beforeend', ARROW_HTML); 478 479 // íì± ì»¬ë¼ìë§ data-order를 ë¨ê²¨ íì¬ ì ë ¬ ìí를 DOMì ëë¬ë¸ë¤ 480 // order ìì´ sortë§ ìë URLì ìë²ê° ë´ë¦¼ì°¨ìì¼ë¡ ì²ë¦¬íë¯ë¡ íìë 거기ì ë§ì¶ë¤ 481 if (sortInput.value && header.dataset.sort == sortInput.value) { 482 header.dataset.order = orderInput.value || 'desc'; 483 } 484 485 paintArrows(header); 486 }); 487 488 document.addEventListener('click', (e) => { 489 let header = e.target.closest('.sort-header'); 490 if (!header) return; 491 492 // íì± í¤ëìë§ data-orderê° ìì¼ë¯ë¡, ìì¼ë©´ ë¤ë¥¸ 컬ë¼ì´ê±°ë í´ì ìíë¤ 493 // í¤ë를 ë르면 ë´ë¦¼ì°¨ì â ì¤ë¦ì°¨ì â í´ì 3ë¨ê³ë¥¼ ìííë¤ 494 let order = header.dataset.order; 495 let nextOrder = !order ? 'desc' : (order == 'desc' ? 'asc' : ''); 496 497 sortInput.value = nextOrder ? header.dataset.sort : ''; // í´ì íë©´ ìë² ê¸°ë³¸ ì ë ¬ë¡ ëìê°ë¤ 498 orderInput.value = nextOrder; 499 500 searchForm.requestSubmit(); // ì ë ¬ì´ ë°ëë©´ page íë¼ë¯¸í°ê° ë¹ ì ¸ 1íì´ì§ë¶í° ë¤ì ì¡°íëë¤ 501 }); 502 503 /** 504 * ì ë ¬ ê° ì ë¬ì© hidden inputì searchForm ìì ë³´ì¥íë¤. 505 * í¼ ë°ì ìì¼ë©´ csv.jsì FormDataì ì ì¡í ë¤ì´ë¡ëë§ ì¡°ì©í 기본 ì ë ¬ë¡ ëê°ë¯ë¡, 506 * í í릿ì ë§¡ê¸°ì§ ìê³ ì¬ê¸°ì ë§ë ë¤. 507 */ 508 function ensureHiddenInput(name, value) { 509 let input = searchForm.querySelector(`[name="${name}"]`); 510 511 if (!input) { 512 input = document.createElement('input'); 513 input.type = 'hidden'; 514 input.name = name; 515 searchForm.appendChild(input); 516 } 517 518 input.value = value ?? ''; 519 520 return input; 521 } 522 523 /** 524 * íì´í ëë ê°±ì . íì´íë íì ë³´ì´ë, íì± ë°©í¥ë§ ì§íê² ê°ì¡°íê³ 525 * ë머ì§(ë°ë ë°©í¥Â·ì ë ¬ ì ë í¤ë)ë íë¦¬ê² ëë¤. 526 */ 527 function paintArrows(header) { 528 let order = header.dataset.order; 529 530 header.querySelectorAll('.sort-arrow').forEach((arrow) => { 531 let active = order && arrow.dataset.order == order; 532 533 arrow.classList.remove('opacity-25', 'opacity-75'); 534 arrow.classList.add(active ? 'opacity-75' : 'opacity-25'); 535 }); 536 } 537}
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.