Files
ai_platform/public/js/learning-infinite.js

228 lines
8.0 KiB
JavaScript

/**
* 학습센터 / 뉴스레터 목록: 무한 스크롤·실시간 검색
* (인라인 스크립트는 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 = '<p class="empty" id="lecture-empty-msg">등록된 항목이 없습니다.</p>';
} else {
var nextP = data.nextPage != null ? data.nextPage : 2;
var sentinelBlock = data.hasNext
? '<div id="lecture-infinite-footer"><div id="infinite-scroll-sentinel" class="infinite-scroll-sentinel" data-next-page="' +
String(nextP) +
'" data-has-next="true"></div><p class="infinite-scroll-loading" id="infinite-scroll-loading" style="display:none;text-align:center;padding:16px;color:#666;">불러오는 중...</p><div class="lecture-load-more-wrap" id="lecture-load-more-wrap"><button type="button" class="lecture-load-more-btn" id="lecture-load-more-btn">더 불러오기</button></div></div>'
: "";
resultsRoot.innerHTML = '<div class="lecture-grid" id="lecture-grid">' + data.html + "</div>" + 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();
})();