1function debounce(func, delay) { 2 let timer; 3 return function(...args) { 4 clearTimeout(timer); 5 timer = setTimeout(() => { 6 func.apply(this, args); 7 }, delay); 8 } 9} 10function throttle(func, delay, trailing = true) { 11 let isThrottled = false; 12 let trailingTimer; 13 return function(...args) { 14 if (trailing) { 15 clearTimeout(trailingTimer); 16 trailingTimer = setTimeout(() => { 17 func.apply(this, args); 18 }, delay); 19 } 20 if (isThrottled) return; 21 func.apply(this, args); 22 isThrottled = true; 23 setTimeout(() => { 24 isThrottled = false; 25 }, delay); 26 } 27} 28function focusTrap(trap) { 29 const selectors = 'a, button, [tabindex="0"], input, textarea, select'; 30 31 trap.addEventListener("keydown", (event) => { 32 if (event.key !== "Tab") return; 33 34 const focusables = [...trap.querySelectorAll(selectors)].filter(el => el.tabIndex !== -1 && !el.disabled); 35 36 if (!focusables.length) return; 37 38 const first = focusables[0]; 39 const last = focusables[focusables.length - 1]; 40 41 if (event.shiftKey && (document.activeElement === first || document.activeElement === trap)) { 42 event.preventDefault(); 43 last.focus(); 44 } else if (!event.shiftKey && document.activeElement === last) { 45 event.preventDefault(); 46 first.focus(); 47 } 48 }); 49 50} 51 52 53 54//////////////////////////////////////// 55////// ëë¡ë¤ì´ 56;(function() { // ëë¡ë¤ì´ 57 var $dropBtn = $(".drop-btn"); 58 if (!$dropBtn.length) return; 59 60 //// ëë¡ë¤ì´ ë«ê¸° 61 function closeDrop() { 62 $dropBtn.removeClass("active"); 63 $dropBtn.attr("aria-expanded", "false"); 64 $dropBtn.next(".drop-menu").removeClass("active"); 65 } 66 67 //// ëë¡ë¤ì´ ë²í¼ ì¤ì 68 $dropBtn.each(function() { 69 var $this = $(this); 70 var menu = this.nextElementSibling; 71 if (!menu) return; 72 73 //// Aria ì¤ì 74 $this.attr("aria-expanded", "false"); 75 76 //// ëë¡ë¤ì´ ì´ê¸° 77 $this.on("click", function() { 78 if ($(this).attr("aria-expanded") == "false") { 79 closeDrop(); 80 $(this).addClass("active"); 81 $(this).attr("aria-expanded", "true"); 82 $(this).next(".drop-menu").addClass("active"); 83 } else { 84 closeDrop(); 85 } 86 }); 87 88 //// ëë¡ë¤ì´ í¬ì»¤ì¤ ìì ì²ë¦¬ 89 $this.on("focusout", function(e) { 90 if (!e.relatedTarget) return; 91 var isFocusInside = menu.contains(e.relatedTarget) || $this.get(0).contains(e.relatedTarget); 92 if (!isFocusInside) closeDrop(); 93 }); 94 menu.addEventListener("focusout", function(e) { 95 if (!e.relatedTarget) return; 96 var isFocusInside = menu.contains(e.relatedTarget) || $this.get(0).contains(e.relatedTarget); 97 if (!isFocusInside) closeDrop(); 98 }); 99 menu.addEventListener("click", function() { 100 closeDrop(); 101 $this[0].focus(); 102 }); 103 104 }); 105 106 ///// ëë¡ë¤ì´ ì¸ë¶ í´ë¦ ì ë«ê¸° 107 $(document).on("click", function(e) { 108 if (!$(e.target).closest(".drop-btn").length) { 109 closeDrop(); 110 } 111 }); 112 $(document).on("keydown", function(e) { 113 if (e.key === "Escape" || e.key === "Esc") { 114 var $dropBtn = $(e.target).closest(".drop-menu").prev(".drop-btn"); 115 if ($dropBtn.length) $dropBtn[0].focus(); 116 closeDrop(); 117 } 118 }); 119})(); 120 121 122//////////////////////////////////////// 123////// Swiper 커ì¤í 124class SwiperCustom { 125 126 // ê¸°ë¥ ìµì ê¸°ë³¸ê° 127 static defaults = { 128 autoBtn: null, 129 timer: false, 130 edgeClass: false, 131 slideFocus: true, 132 bulletsAria: true, 133 }; 134 static config = { 135 timerVar: 'auto-progress', 136 }; 137 138 /** 139 * @version 2.4.2 140 * @param {Swiper} swiper - Swiper ì¸ì¤í´ì¤ 141 * @param {SwiperCustomOption} [option] - ìµì 142 * 143 * @example 144 * new SwiperCustom(mySwiper, { 145 * autoBtn: null, 146 * timer: false, 147 * timerVar: 'auto-progress', 148 * edgeClass: false, 149 * slideFocus: true, 150 * bulletsAria: true, 151 * }); 152 */ 153 constructor(swiper, option = {}) { 154 if (Array.isArray(swiper)) { 155 swiper.forEach(sw => new SwiperCustom(sw, option)); 156 return; 157 } 158 159 if (!swiper || !swiper.el) { 160 const target = swiper?.originalParams?.el || swiper?.params?.el || swiper; 161 console.warn(`SwiperCustom: ì í¨í Swiperê° ìëëë¤. (${target})`); 162 return; 163 } 164 165 this.swiper = swiper; 166 this.autoBtnEls = null; 167 this.timerEls = null; 168 this._listeners = []; 169 this._wasRunning = null; 170 171 this.option = { ...SwiperCustom.defaults, ...SwiperCustom.config, ...option }; 172 173 this.setup(); 174 175 this.swiper.on('beforeDestroy', () => {
176 this._listeners.forEach(fn => fn()); 177 this._listeners = []; 178 this.autoBtnEls = null; 179 this.timerEls = null; 180 }); 181 } 182 183 184 // âââ ë´ë¶ ì í¸ë¦¬í° ââââââââââââââââââââââââââââââââââââââ 185 186 setup() { 187 for (const key in SwiperCustom.defaults) { 188 if (!this.option[key]) continue; 189 const method = `init${key[0].toUpperCase()}${key.slice(1)}`; 190 if (typeof this[method] === 'function') this[method](); 191 } 192 193 this.on('autoplayStart', () => { 194 if (this.swiper.isLocked) { 195 this.swiper.autoplay?.stop(); 196 return; 197 } 198 this.updateAutoState(); 199 }); 200 this.on('autoplayStop', () => { 201 this.resetTimerVar(); 202 this.updateAutoState(); 203 }); 204 this.on('lock', () => { 205 this.stopForLock(); 206 }); 207 this.on('unlock', () => { 208 this.updateLockClass(); 209 if (this._wasRunning) { 210 this.swiper.autoplay?.start(); 211 } 212 }); 213 this.on('breakpoint', () => { 214 if (!this.swiper.isLocked && this.swiper.params.autoplay?.enabled) { 215 this.swiper.autoplay?.start(); 216 } else { 217 this.swiper.autoplay?.stop(); 218 } 219 }); 220 221 if (this.swiper.isLocked) { 222 this.stopForLock(); 223 } else { 224 this.updateAutoState(); 225 } 226 } 227 228 /** 229 * CSS ì íì ëë DOM ìì를 ë°ì ë°°ì´ë¡ ë°í 230 * swiper.el â ìì 3ë¨ê³ â document ììë¡ íì 231 */ 232 getEl(target) { 233 const list = Array.isArray(target) ? target : [target]; 234 return list.map(t => { 235 if (t instanceof Element) return t; 236 if (typeof t !== 'string') return null; 237 238 let el = this.swiper.el; 239 for (let i = 0; i < 3; i++) { 240 const found = el?.querySelector(t); 241 if (found) return found; 242 el = el?.parentElement; 243 } 244 return document.querySelector(t); 245 }).filter(Boolean); 246 } 247 248 /** swiper ì´ë²¤í¸ ë±ë¡ + destroy ì ìë í´ì */ 249 on(event, handler) { 250 this.swiper.on(event, handler); 251 this._listeners.push(() => this.swiper.off(event, handler)); 252 } 253 254 /** ì¸ë¶ DOM ì´ë²¤í¸ ë±ë¡ + destroy ì ìë í´ì */ 255 listen(target, event, handler, options) { 256 target.addEventListener(event, handler, options); 257 this._listeners.push(() => target.removeEventListener(event, handler, options)); 258 } 259 260 /** ìëì¬ì ë²í¼Â·íì´ë¨¸ ìí UI ë기í, wrapperElì aria-live ì¸í ë³´ì */ 261 updateAutoState() { 262 const running = !!this.swiper.autoplay?.running; 263 this.autoBtnEls?.forEach(btn => { 264 btn.setAttribute('aria-pressed', running); 265 btn.classList.toggle('paused', !running); 266 }); 267 this.swiper.wrapperEl.setAttribute('aria-live', running ? 'off' : 'polite'); 268 } 269 270 /** timer ì´ê¸°í */ 271 resetTimerVar() { 272 this.timerEls?.forEach(el => el.style.setProperty('--' + this.option.timerVar, '0')); 273 } 274 275 /** lock ì autoplay ì ì§ + ìí ì ì¥ */ 276 stopForLock() { 277 this._wasRunning = !!this.swiper.autoplay?.running; 278 this.swiper.autoplay?.stop(); 279 this.resetTimerVar(); 280 this.updateAutoState(); 281 this.updateLockClass(); 282 } 283 284 /** lock ìíì ë°ë¼ autoBtnElsì lockClass í ê¸ */ 285 updateLockClass() { 286 const lockClass = this.swiper.params?.navigation?.lockClass || 'swiper-button-lock'; 287 this.autoBtnEls?.forEach(btn => { 288 btn.classList.toggle(lockClass, this.swiper.isLocked); 289 }); 290 } 291 292 293 // âââ ê¸°ë¥ ì´ê¸°í (ìµì ë³) âââââââââââââââââââââââââââââââ 294 /** 295 * @typedef {Object} SwiperCustomOption 296 * 297 * @property {string|Element|Array} [autoBtn=null] 298 * ìëì¬ì ë²í¼ ì§ì (aria-pressed ìí ì ë¬) 299 * - `null`: ìëì¬ì ë²í¼ 미ì ì© **(기본ê°)** 300 * - `'ì íì'`: CSS ì íì 문ìì´ 301 * - `DOM`: document.querySelector() ë±ì¼ë¡ ê°ì ¸ì¨ ìì 302 * - `['ì íì', DOM, ...]`: ì¬ë¬ ê°ì¼ ë ë°°ì´ë¡ ì ë¬ 303 * 304 * **â»`aria-label` 미ì¤ì ì `aria-label="Autoplay"` ìë ë¶ì¬** 305 * 306 * @property {boolean|string|Element|Array} [timer=false] 307 * ìëì¬ì ì§íë CSSë³ì(`--timerVar`) ì ì© ëì ì§ì 308 * - `false`: 미ì ì© **(기본ê°)** 309 * - `true`: swiper.el + autoBtnì ì ì© 310 * - `'ì íì'`: CSS ì íì 문ìì´ 311 * - `DOM`: document.querySelector() ë±ì¼ë¡ ê°ì ¸ì¨ ìì 312 * - `['ì íì', DOM, ...]`: ì¬ë¬ ê°ì¼ ë ë°°ì´ë¡ ì ë¬ 313 * 314 * @property {string} [timerVar='--auto-progress'] 315 * ìëì¬ì íì´ë¨¸ CSSë³ì ì´ë¦ ì§ì 316 * - **기본ê°**: `'auto-progress'` 317 * - ìì: `'swiper-autoplay-timer'` 318 * 319 * @property {boolean|string|Element} [edgeClass=false] 320 * Swiper ì§íëì ë°ë¼ í´ëì¤ ì ë°ì´í¸ 321 * - `false`: 미ì ì© **(기본ê°)** 322 * - `true`: swiper.elì ì ì© 323 * - `'ì íì'`: CSS ì íì 문ìì´ 324 * - `DOM`: document.querySelector() ë±ì¼ë¡ ê°ì ¸ì¨ ìì 325 * 326 * **â»`loop` ì¤ì ì is-begin/is-endë 미ì ì©** 327 * 328 * @property {boolean} [slideFocus=true] 329 * - `true`: ë³´ì´ë ì¬ë¼ì´ëë§ í¬ì»¤ì± **(기본ê°)** 330 * - `false`: 모ë ì¬ë¼ì´ë í¬ì»¤ì± 331 */ 332 333 initAutoBtn() { 334 this.autoBtnEls = this.getEl(this.option.autoBtn); 335 if (!this.autoBtnEls.length) { 336 console.warn('SwiperCustom Warning: autoBtn ìì를 ì°¾ì ì ììµëë¤.', this.swiper.el); 337 return; 338 } 339
340 this.autoBtnEls.forEach(btn => { 341 if (!btn.hasAttribute('aria-label')) btn.setAttribute('aria-label', 'Autoplay'); 342 const wrapperId = this.swiper.wrapperEl.id; 343 if (wrapperId) btn.setAttribute('aria-controls', wrapperId); 344 this.listen(btn, 'click', () => { 345 if (this.swiper.isLocked) return; 346 this.swiper.autoplay?.running 347 ? this.swiper.autoplay?.stop() 348 : this.swiper.autoplay?.start(); 349 }); 350 }); 351 } 352 353 initTimer() { 354 this.timerEls = []; 355 if (this.option.timer === true) { 356 this.timerEls.push(this.swiper.el); 357 if (this.autoBtnEls?.length) this.timerEls.push(...this.autoBtnEls); 358 } else { 359 this.timerEls.push(...this.getEl(this.option.timer)); 360 if (!this.timerEls.length) { 361 console.warn('SwiperCustom Warning: timer ìì를 ì°¾ì ì ììµëë¤.', this.swiper.el); 362 } 363 } 364 365 if (this.timerEls.length) { 366 this.on('autoplayTimeLeft', (s, t, p) => { 367 const value = (1 - p).toFixed(4); 368 this.timerEls.forEach(el => el.style.setProperty('--' + this.option.timerVar, value)); 369 }); 370 } 371 } 372 373 initEdgeClass() { 374 const sw = this.swiper; 375 const target = this.option.edgeClass === true 376 ? sw.el 377 : this.getEl(this.option.edgeClass)[0]; 378 379 if (!target) return; 380 381 const update = () => { 382 if (!sw.params.loop) { 383 target.classList.toggle('is-begin', sw.progress === 0); 384 target.classList.toggle('is-end', sw.progress === 1); 385 } 386 target.classList.toggle('is-overflow', !sw.isLocked); 387 }; 388 389 update(); 390 this.on('progress', update); 391 } 392 393 initSlideFocus() { 394 const sw = this.swiper; 395 const visibleClass = sw.params.slideVisibleClass || 'swiper-slide-visible'; 396 sw.params.watchSlidesProgress = true; 397 sw.update(); 398 399 const focusableSelector = 'a, button, input, select, textarea, [tabindex], iframe, video, audio'; 400 401 const update = () => { 402 if (!sw.slides?.length) return;
403 sw.slides.forEach(slide => { 404 const hidden = !slide.classList.contains(visibleClass); 405 slide.setAttribute('aria-hidden', String(hidden)); 406 if (hidden) { 407 slide.setAttribute('tabindex', '-1'); 408 slide.querySelectorAll(focusableSelector).forEach(el => el.setAttribute('tabindex', '-1')); 409 } else { 410 slide.removeAttribute('tabindex'); 411 slide.querySelectorAll(focusableSelector).forEach(el => el.removeAttribute('tabindex')); 412 } 413 }); 414 }; 415 416 update(); 417 this.on('slideChangeTransitionStart', update); 418 this.on('observerUpdate', update); 419 this.on('resize', update); 420 this.listen(window, 'load', update); 421 } 422 423 initBulletsAria() { 424 if (!this.swiper.pagination?.bullets?.length) return; 425 const activeClass = this.swiper.pagination.bulletActiveClass || 'swiper-pagination-bullet-active'; 426 const update = () => { 427 const bullets = this.swiper.pagination.bullets; 428 if (!bullets?.length) return; 429 bullets.forEach(bullet => bullet.removeAttribute('title')); 430 const active = [...bullets].find(b => b.classList.contains(activeClass)); 431 active?.setAttribute('title', 'ì íë¨'); 432 }; 433 update(); 434 this.on('slideChange', update); 435 } 436} 437 438 439//////////////////////////////////////// 440////// í 441const ExTab = (function () { 442 'use strict'; 443 444 const DEFAULTS = { 445 tabBtn: '.tab-btn', 446 activeClass: 'active', 447 defaultIndex: 0, 448 onChange: null, 449 }; 450 451 class ExTab { 452 constructor(el, options = {}) { 453 if (typeof el === 'string') el = document.querySelector(el); 454 if (!el || el._tabInstance) return; 455 456 this.el = el; 457 this.options = Object.assign({}, DEFAULTS, options); 458 this.tabs = []; 459 this.currentIndex = -1; 460 461 el._tabInstance = this; 462 this._init(); 463 } 464 465 _init() { 466 const o = this.options; 467 const btns = this.el.querySelectorAll(o.tabBtn); 468 469 if (!btns.length) return; 470 471 const firstParent = btns[0].parentElement; 472 this.list = firstParent.tagName === 'LI' ? firstParent.parentElement : firstParent; 473 474 btns.forEach((btn, i) => { 475 const li = btn.closest('li'); 476 const controlsId = btn.getAttribute('aria-controls'); 477 const panel = controlsId ? document.getElementById(controlsId) : null; 478 const isTabItem = !!controlsId; 479 480 this.tabs.push({ btn, li, panel, isTabItem }); 481 482 if (isTabItem) { 483 btn.addEventListener('click', (e) => { 484 e.preventDefault(); 485 this.goTo(i, true); 486 }); 487 } 488 }); 489 490 this._setupAria(); 491 this._activateDefault(); 492 } 493 494 // ââ ARIA ââ 495 496 _setupAria() { 497
498 this.tabs.forEach((item, i) => { 499 if (!item.isTabItem) return; 500 501 item.btn.setAttribute('aria-current', 'false'); 502 }); 503 } 504 505 // ââ ì´ê¸° íì±í ââ 506 507 _activateDefault() { 508 const o = this.options; 509 const tabItems = this.tabs.filter(t => t.isTabItem); 510 511 if (!tabItems.length) return; 512 513 let activeIdx = -1; 514 515 this.tabs.forEach((item, i) => { 516 if (item.isTabItem && item.btn.classList.contains(o.activeClass)) { 517 activeIdx = i; 518 } 519 }); 520 521 if (activeIdx === -1 && o.defaultIndex >= 0) { 522 const target = tabItems[o.defaultIndex]; 523 if (target) activeIdx = this.tabs.indexOf(target); 524 } 525 526 if (activeIdx !== -1) { 527 this.goTo(activeIdx, false); 528 } 529 } 530 531 // ââ ê³µê° ë©ìë ââ 532 533 goTo(index, isFocus) { 534 const o = this.options; 535 const target = this.tabs[index]; 536 if (!target || !target.isTabItem) return; 537 538 const prevIndex = this.currentIndex; 539
540 this.tabs.forEach((item) => { 541 if (!item.isTabItem) return; 542 543 item.li?.classList.remove(o.activeClass); 544 item.btn.classList.remove(o.activeClass); 545 item.btn.setAttribute('aria-current', 'false'); 546 547 if (item.panel) { 548 item.panel.classList.remove(o.activeClass); 549 } 550 }); 551 552 target.li?.classList.add(o.activeClass); 553 target.btn.classList.add(o.activeClass); 554 target.panel.classList.add(o.activeClass); 555 556 target.btn.setAttribute('aria-current', 'true'); 557 558 this.currentIndex = index; 559 560 if (isFocus) target.btn.focus(); 561 562 if (prevIndex !== index && typeof o.onChange === 'function') { 563 o.onChange(index, target.btn, target.panel); 564 } 565 } 566 567 static init(selector, options = {}) { 568 return Array.from(document.querySelectorAll(selector)).map( 569 el => new ExTab(el, options) 570 ); 571 } 572 } 573 574 return ExTab; 575})(); 576 577 578;(() => { // ë°°ë 모ì 579 const banner = document.querySelector('.bottom-banner'); 580 if (!banner) return; 581 582 let swiper = null; 583 let swiperMode = null; 584 const swiperEl = banner.querySelector('.swiper'); 585 const wrapperEl = banner.querySelector('.swiper-wrapper'); 586 if (!swiperEl) return; 587 588 const bannerInit = () => { 589 590 if (swiper) { 591 swiper.destroy(); 592 swiper = null; 593 } 594 595 // ì¬ë¼ì´ë ìê° ë§ì¼ë©´ loop, ì ì¼ë©´ rewind 596 if (wrapperEl.scrollWidth > swiperEl.offsetWidth * 1.5) { 597 swiperMode = { 598 loop: true, 599 loopAdditionalSlides: 1, 600 }; 601 swiperEl.dataset.mode = 'loop'; 602 } else { 603 swiperMode = { 604 rewind: true, 605 }; 606 swiperEl.dataset.mode = 'rewind'; 607 } 608 609 swiper = new Swiper(swiperEl, { 610 ...swiperMode, 611 slidesPerView: 'auto', 612 speed: 600, 613 autoplay: { 614 delay: 5000, 615 disableOnInteraction: false, 616 }, 617 navigation: { 618 nextEl: banner.querySelector('.swiper-btn.next'), 619 prevEl: banner.querySelector('.swiper-btn.prev'), 620 }, 621 }); 622 new SwiperCustom(swiper, { 623 autoBtn: banner.querySelector('.swiper-btn.toggle'), 624 edgeClass: true, 625 }); 626 } 627 bannerInit(); 628 629 let prevWidth = window.innerWidth; 630 window.addEventListener('resize', debounce(() => { 631 const currentWidth = window.innerWidth; 632 if (currentWidth !== prevWidth) { 633 prevWidth = currentWidth; 634 bannerInit(); 635 } 636 }, 200)); 637 638})(); 639 640 641;(() => { // íì´ì§ ì¤í¬ë¡¤ ê°ì§ 642 const html = document.documentElement; 643 644 const TOL = 100; 645 function setPageClass() { 646 const atEnd = window.scrollY + window.innerHeight >= html.scrollHeight - TOL; 647 html.classList.toggle("is-scroll-end", atEnd); 648 html.classList.toggle("is-scrolling", window.scrollY > 100); 649 } 650 651 window.addEventListener("scroll", throttle(setPageClass, 100)); 652 if (window.visualViewport) { 653 window.visualViewport.addEventListener("scroll", setPageClass); 654 } 655 656})();
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.