Initial commit: AI platform app (server, views, lib, data, deploy docs)
Made-with: Cursor
This commit is contained in:
BIN
public/images/aiplatform-logo.png
Normal file
BIN
public/images/aiplatform-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
public/images/xavis-logo.png
Normal file
BIN
public/images/xavis-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
227
public/js/learning-infinite.js
Normal file
227
public/js/learning-infinite.js
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 학습센터 / 뉴스레터 목록: 무한 스크롤·실시간 검색
|
||||
* (인라인 스크립트는 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();
|
||||
})();
|
||||
Binary file not shown.
Binary file not shown.
BIN
public/resources/ai-success/자비스_AI성공사례_조정숙과장.pdf
Normal file
BIN
public/resources/ai-success/자비스_AI성공사례_조정숙과장.pdf
Normal file
Binary file not shown.
3250
public/styles.css
Normal file
3250
public/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user