/** * 학습센터 / 뉴스레터 목록: 무한 스크롤·실시간 검색 * (인라인 스크립트는 HTML 엔티티(&) 처리로 SyntaxError가 날 수 있어 외부 파일로 분리) */ (function () { var cfg = document.getElementById("lecture-page-config"); var apiPath = (cfg && cfg.getAttribute("data-learning-api")) || "/api/learning/lectures"; var viewerBasePath = (cfg && cfg.getAttribute("data-viewer-base")) || "/learning"; var form = document.querySelector(".filter-panel form"); var qInput = document.getElementById("learning-filter-q"); var resultsRoot = document.getElementById("lecture-results-root"); var countEl = document.getElementById("lecture-total-count"); var radios = document.querySelectorAll('#category-filter input[name="category"]'); if (form && radios.length) { radios.forEach(function (radio) { radio.addEventListener("change", function () { form.submit(); }); }); } var scrollObserver = null; var scrollLoading = false; var scrollListener = null; var scrollRootsBound = []; var wheelHandler = null; var pollTimer = null; function disconnectInfiniteScroll() { if (scrollObserver) { scrollObserver.disconnect(); scrollObserver = null; } if (scrollListener) { scrollRootsBound.forEach(function (t) { t.removeEventListener("scroll", scrollListener); }); scrollRootsBound = []; window.removeEventListener("resize", scrollListener); scrollListener = null; } if (wheelHandler) { document.removeEventListener("wheel", wheelHandler, true); wheelHandler = null; } if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } function setupInfiniteScroll() { disconnectInfiniteScroll(); var loadingEl = document.getElementById("infinite-scroll-loading"); function shouldLoadMore() { var sentinel = document.getElementById("infinite-scroll-sentinel"); if (!sentinel || sentinel.getAttribute("data-has-next") !== "true") return false; var rect = sentinel.getBoundingClientRect(); var vh = window.innerHeight || document.documentElement.clientHeight; var margin = 3200; return rect.top <= vh + margin; } function loadNextPage() { if (scrollLoading) return; var sentinel = document.getElementById("infinite-scroll-sentinel"); var grid = document.getElementById("lecture-grid"); if (!sentinel || !grid) return; if (sentinel.getAttribute("data-has-next") !== "true") return; var nextPage = sentinel.getAttribute("data-next-page"); if (!nextPage) return; scrollLoading = true; if (loadingEl) loadingEl.style.display = "block"; var params = new URLSearchParams(window.location.search); params.set("page", nextPage); fetch(apiPath + "?" + params.toString(), { credentials: "same-origin", headers: { Accept: "application/json" }, }) .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (data) { if (data.html) { grid.insertAdjacentHTML("beforeend", data.html); } if (data.hasNext) { sentinel.setAttribute("data-next-page", String(data.nextPage || parseInt(nextPage, 10) + 1)); sentinel.setAttribute("data-has-next", "true"); } else { disconnectInfiniteScroll(); var foot = document.getElementById("lecture-infinite-footer"); if (foot && foot.parentNode) foot.remove(); } }) .catch(function (e) { var msg = ""; if (e) { if (e.message) msg = e.message; else msg = String(e); } console.warn("[learning] 다음 페이지 로드 실패(재시도 가능):", msg); }) .finally(function () { scrollLoading = false; if (loadingEl) loadingEl.style.display = "none"; setTimeout(function () { if (shouldLoadMore()) loadNextPage(); }, 0); }); } function loadMoreIfNeeded() { if (!shouldLoadMore()) return; loadNextPage(); } var sentinelEl = document.getElementById("infinite-scroll-sentinel"); var gridEl = document.getElementById("lecture-grid"); if (!sentinelEl || !gridEl) return; var rafScheduled = false; scrollListener = function () { if (scrollLoading || rafScheduled) return; rafScheduled = true; requestAnimationFrame(function () { rafScheduled = false; loadMoreIfNeeded(); }); }; function addScrollRoot(r) { if (r && scrollRootsBound.indexOf(r) === -1) scrollRootsBound.push(r); } addScrollRoot(window); addScrollRoot(document.scrollingElement || document.documentElement); scrollRootsBound.forEach(function (t) { t.addEventListener("scroll", scrollListener, { passive: true }); }); window.addEventListener("resize", scrollListener, { passive: true }); wheelHandler = function () { if (scrollLoading) return; requestAnimationFrame(loadMoreIfNeeded); }; document.addEventListener("wheel", wheelHandler, { passive: true, capture: true }); pollTimer = setInterval(function () { if (scrollLoading) return; loadMoreIfNeeded(); }, 400); scrollObserver = new IntersectionObserver( function (entries) { if (!entries.some(function (e) { return e.isIntersecting; })) return; loadNextPage(); }, { root: null, rootMargin: "0px 0px 1600px 0px", threshold: 0 } ); scrollObserver.observe(sentinelEl); setTimeout(loadMoreIfNeeded, 0); setTimeout(loadMoreIfNeeded, 400); var loadMoreBtn = document.getElementById("lecture-load-more-btn"); if (loadMoreBtn) { loadMoreBtn.addEventListener("click", function () { loadNextPage(); }); } } function applyLiveSearch() { if (!form || !resultsRoot) return; var params = new URLSearchParams(new FormData(form)); params.set("page", "1"); fetch(apiPath + "?" + params.toString()) .then(function (r) { return r.json(); }) .then(function (data) { if (data.error) return; var total = typeof data.totalCount === "number" ? data.totalCount : 0; if (countEl) countEl.textContent = total; var html = (data.html || "").trim(); if (total === 0 || !html) { disconnectInfiniteScroll(); resultsRoot.innerHTML = '

등록된 항목이 없습니다.

'; } else { var nextP = data.nextPage != null ? data.nextPage : 2; var sentinelBlock = data.hasNext ? '' : ""; resultsRoot.innerHTML = '
' + data.html + "
" + sentinelBlock; setupInfiniteScroll(); } if (window.history && window.history.replaceState) { var forUrl = new URLSearchParams(params); forUrl.delete("page"); var u = new URL(viewerBasePath, window.location.origin); u.search = forUrl.toString() ? "?" + forUrl.toString() : ""; window.history.replaceState({}, "", u.pathname + u.search); } }) .catch(function () {}); } var searchDebounce; if (qInput) { qInput.addEventListener("input", function () { clearTimeout(searchDebounce); searchDebounce = setTimeout(applyLiveSearch, 280); }); } setupInfiniteScroll(); })();