/** * 덱 목록 무한 스크롤 페이저 * 필터링된 배열을 화면에 청크 단위로 나눠 렌더링하고, * 하단 센티넬이 뷰포트에 근접하면 다음 청크를 이어 붙인다. */ (function initDeckListPager() { const DEFAULT_PAGE_SIZE = 24; /** * @typedef {object} DeckPagerOptions * @property {HTMLElement} container 목록을 그릴 컨테이너 * @property {(item: object) => string} renderItem 카드 HTML 생성 함수 * @property {() => string} [emptyHtml] 항목이 없을 때 표시할 HTML * @property {(grid: HTMLElement) => void} [onAppend] 청크 추가 후 콜백 * @property {string} [gridClass] 그리드 컨테이너 클래스 * @property {number} [pageSize] 청크당 항목 수 */ /** * 무한 스크롤 페이저 생성 * @param {DeckPagerOptions} options * @returns {{ refresh: (items: object[]) => void }} */ function create(options) { const { container, renderItem, emptyHtml, onAppend, gridClass = 'deck-grid', pageSize = DEFAULT_PAGE_SIZE, } = options; /** @type {object[]} */ let items = []; let rendered = 0; /** @type {HTMLElement|null} */ let grid = null; /** @type {HTMLElement|null} */ let sentinel = null; /** @type {IntersectionObserver|null} */ let observer = null; function teardownObserver() { if (observer) { observer.disconnect(); observer = null; } } function removeSentinel() { teardownObserver(); if (sentinel) { sentinel.remove(); sentinel = null; } } function appendNext() { if (!grid) return; const next = items.slice(rendered, rendered + pageSize); if (next.length) { grid.insertAdjacentHTML('beforeend', next.map(renderItem).join('')); rendered += next.length; if (typeof onAppend === 'function') onAppend(grid); } if (rendered >= items.length) { removeSentinel(); } } /** * 목록을 처음부터 다시 렌더링 * @param {object[]} nextItems */ function refresh(nextItems) { items = Array.isArray(nextItems) ? nextItems : []; rendered = 0; removeSentinel(); container.innerHTML = ''; if (!items.length) { container.innerHTML = typeof emptyHtml === 'function' ? emptyHtml() : ''; grid = null; return; } grid = document.createElement('div'); grid.className = gridClass; container.appendChild(grid); sentinel = document.createElement('div'); sentinel.setAttribute('aria-hidden', 'true'); sentinel.style.cssText = 'width:100%;height:1px;'; container.appendChild(sentinel); appendNext(); if (rendered >= items.length) { removeSentinel(); return; } if ('IntersectionObserver' in window) { observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { appendNext(); break; } } }, { rootMargin: '600px 0px' }, ); observer.observe(sentinel); } else { // IntersectionObserver 미지원 환경은 전체 렌더로 폴백 while (rendered < items.length) appendNext(); } } return { refresh }; } window.UaDeckPager = { create }; })();