Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
// ============================================
|
||||
// 비디오 슬라이드 모듈 (페이지네이션)
|
||||
// 공통 모듈 활용 (ErrorHandler, DOMUtils, EventManager, Utils, AnimationUtils)
|
||||
// ============================================
|
||||
|
||||
class VideoSlider {
|
||||
constructor(config, dependencies = {}) {
|
||||
// 의존성 주입 (폴백 포함)
|
||||
this.domUtils = dependencies.domUtils || (typeof DOMUtils !== 'undefined' ? DOMUtils : null);
|
||||
this.errorHandler = dependencies.errorHandler || (typeof ErrorHandler !== 'undefined' ? ErrorHandler : null);
|
||||
this.eventManager = dependencies.eventManager || (typeof eventManager !== 'undefined' ? eventManager : null);
|
||||
this.utils = dependencies.utils || (typeof Utils !== 'undefined' ? Utils : null);
|
||||
this.animationUtils = dependencies.animationUtils || (typeof AnimationUtils !== 'undefined' ? AnimationUtils : null);
|
||||
|
||||
// 이벤트 리스너 ID 저장 (정리용)
|
||||
this.listenerIds = [];
|
||||
|
||||
try {
|
||||
// 입력값 유효성 검증
|
||||
if (!config || typeof config !== 'object') {
|
||||
this._handleError(new Error('config가 유효하지 않습니다.'), 'constructor');
|
||||
config = {};
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
this.videos = Array.isArray(config.videos) ? config.videos : [];
|
||||
this.currentPage = 0;
|
||||
this.videosPerPage = config.videosPerPage || 6;
|
||||
|
||||
// config에 videosPerPage가 없으면 설정
|
||||
if (!this.config.videosPerPage) {
|
||||
this.config.videosPerPage = this.videosPerPage;
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, 'constructor');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 처리 헬퍼
|
||||
* @private
|
||||
*/
|
||||
_handleError(error, context, additionalInfo = {}) {
|
||||
if (this.errorHandler) {
|
||||
this.errorHandler.handle(error, {
|
||||
context: `VideoSlider.${context}`,
|
||||
component: 'VideoSlider',
|
||||
...additionalInfo
|
||||
}, false);
|
||||
} else {
|
||||
console.error(`[VideoSlider] ${context}:`, error, additionalInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// 초기화
|
||||
init() {
|
||||
try {
|
||||
this.setupEventListeners();
|
||||
this.updatePagination();
|
||||
} catch (error) {
|
||||
this._handleError(error, 'init');
|
||||
}
|
||||
}
|
||||
|
||||
// 이벤트 리스너 설정 (EventManager 활용)
|
||||
setupEventListeners() {
|
||||
try {
|
||||
const prevBtn = this.domUtils?.$("#prevBtn") ||
|
||||
(typeof DOMUtils !== 'undefined' ? DOMUtils.$("#prevBtn") : null) ||
|
||||
document.querySelector("#prevBtn");
|
||||
const nextBtn = this.domUtils?.$("#nextBtn") ||
|
||||
(typeof DOMUtils !== 'undefined' ? DOMUtils.$("#nextBtn") : null) ||
|
||||
document.querySelector("#nextBtn");
|
||||
|
||||
if (!prevBtn || !nextBtn) {
|
||||
console.warn("[VideoSlider] 이전/다음 버튼을 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 이전 버튼 클릭 핸들러
|
||||
const prevHandler = async (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.currentPage > 0) {
|
||||
this.currentPage--;
|
||||
await this.changePage();
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, 'setupEventListeners.prevHandler');
|
||||
}
|
||||
};
|
||||
|
||||
// 다음 버튼 클릭 핸들러
|
||||
const nextHandler = async (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const totalPages = this.getTotalPages();
|
||||
if (this.currentPage < totalPages - 1) {
|
||||
this.currentPage++;
|
||||
await this.changePage();
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, 'setupEventListeners.nextHandler');
|
||||
}
|
||||
};
|
||||
|
||||
// EventManager 사용 (있는 경우)
|
||||
if (this.eventManager) {
|
||||
const prevId = this.eventManager.on(prevBtn, "click", prevHandler);
|
||||
const nextId = this.eventManager.on(nextBtn, "click", nextHandler);
|
||||
this.listenerIds.push(
|
||||
{ element: prevBtn, id: prevId, type: 'click' },
|
||||
{ element: nextBtn, id: nextId, type: 'click' }
|
||||
);
|
||||
} else {
|
||||
// 폴백: 직접 이벤트 리스너 등록
|
||||
prevBtn.addEventListener("click", prevHandler);
|
||||
nextBtn.addEventListener("click", nextHandler);
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, 'setupEventListeners');
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 페이지 영상 가져오기
|
||||
getCurrentPageVideos() {
|
||||
try {
|
||||
if (!Array.isArray(this.videos)) {
|
||||
this._handleError(new Error('videos가 배열이 아닙니다.'), 'getCurrentPageVideos');
|
||||
return [];
|
||||
}
|
||||
|
||||
const videosPerPage = this.config.videosPerPage || this.videosPerPage || 6;
|
||||
const start = this.currentPage * videosPerPage;
|
||||
const end = start + videosPerPage;
|
||||
return this.videos.slice(start, end);
|
||||
} catch (error) {
|
||||
this._handleError(error, 'getCurrentPageVideos');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 변경 (AnimationUtils 활용)
|
||||
async changePage() {
|
||||
try {
|
||||
const container = this.domUtils?.$("#videoCardsContainer") ||
|
||||
(typeof DOMUtils !== 'undefined' ? DOMUtils.$("#videoCardsContainer") : null) ||
|
||||
document.querySelector("#videoCardsContainer");
|
||||
|
||||
if (!container) {
|
||||
this._handleError(new Error('videoCardsContainer를 찾을 수 없습니다.'), 'changePage');
|
||||
return;
|
||||
}
|
||||
|
||||
// 페이드 아웃 효과
|
||||
if (this.animationUtils && this.animationUtils.fade) {
|
||||
await this.animationUtils.fade(container, "out", 400);
|
||||
} else if (typeof AnimationUtils !== 'undefined' && AnimationUtils.fade) {
|
||||
await AnimationUtils.fade(container, "out", 400);
|
||||
} else {
|
||||
// 폴백: 직접 페이드 효과
|
||||
container.style.opacity = '0';
|
||||
await (this.utils?.delay(400) || new Promise(resolve => setTimeout(resolve, 400)));
|
||||
}
|
||||
|
||||
// 외부 렌더링 함수 호출
|
||||
if (this.config.onPageChange && typeof this.config.onPageChange === 'function') {
|
||||
try {
|
||||
this.config.onPageChange(this.getCurrentPageVideos());
|
||||
} catch (error) {
|
||||
this._handleError(error, 'changePage.onPageChange');
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지네이션 업데이트
|
||||
this.updatePagination();
|
||||
|
||||
// 페이드 인 효과
|
||||
if (this.animationUtils && this.animationUtils.fade) {
|
||||
await this.animationUtils.fade(container, "in", 400);
|
||||
} else if (typeof AnimationUtils !== 'undefined' && AnimationUtils.fade) {
|
||||
await AnimationUtils.fade(container, "in", 400);
|
||||
} else {
|
||||
// 폴백: 직접 페이드 효과
|
||||
container.style.opacity = '1';
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, 'changePage');
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지네이션 업데이트
|
||||
updatePagination() {
|
||||
try {
|
||||
const pagination = this.domUtils?.$("#pagination") ||
|
||||
(typeof DOMUtils !== 'undefined' ? DOMUtils.$("#pagination") : null) ||
|
||||
document.querySelector("#pagination");
|
||||
const prevBtn = this.domUtils?.$("#prevBtn") ||
|
||||
(typeof DOMUtils !== 'undefined' ? DOMUtils.$("#prevBtn") : null) ||
|
||||
document.querySelector("#prevBtn");
|
||||
const nextBtn = this.domUtils?.$("#nextBtn") ||
|
||||
(typeof DOMUtils !== 'undefined' ? DOMUtils.$("#nextBtn") : null) ||
|
||||
document.querySelector("#nextBtn");
|
||||
|
||||
if (!pagination) {
|
||||
console.warn("[VideoSlider] pagination 요소를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = this.getTotalPages();
|
||||
const currentPageNum = Math.max(0, Math.min(this.currentPage + 1, totalPages));
|
||||
|
||||
// XSS 방지를 위해 텍스트로 설정
|
||||
pagination.innerHTML = '';
|
||||
const currentSpan = document.createElement('span');
|
||||
currentSpan.className = 'current';
|
||||
currentSpan.textContent = currentPageNum;
|
||||
pagination.appendChild(currentSpan);
|
||||
pagination.appendChild(document.createTextNode(` / ${totalPages}`));
|
||||
|
||||
// 이전 버튼 상태
|
||||
if (prevBtn) {
|
||||
const isDisabled = this.currentPage === 0;
|
||||
if (this.domUtils && this.domUtils.toggleClass) {
|
||||
this.domUtils.toggleClass(prevBtn, "disabled", isDisabled);
|
||||
} else if (typeof DOMUtils !== 'undefined' && DOMUtils.toggleClass) {
|
||||
DOMUtils.toggleClass(prevBtn, "disabled", isDisabled);
|
||||
} else {
|
||||
// 폴백
|
||||
if (isDisabled) {
|
||||
prevBtn.classList.add("disabled");
|
||||
} else {
|
||||
prevBtn.classList.remove("disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 버튼 상태
|
||||
if (nextBtn) {
|
||||
const isDisabled = this.currentPage >= totalPages - 1;
|
||||
if (this.domUtils && this.domUtils.toggleClass) {
|
||||
this.domUtils.toggleClass(nextBtn, "disabled", isDisabled);
|
||||
} else if (typeof DOMUtils !== 'undefined' && DOMUtils.toggleClass) {
|
||||
DOMUtils.toggleClass(nextBtn, "disabled", isDisabled);
|
||||
} else {
|
||||
// 폴백
|
||||
if (isDisabled) {
|
||||
nextBtn.classList.add("disabled");
|
||||
} else {
|
||||
nextBtn.classList.remove("disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, 'updatePagination');
|
||||
}
|
||||
}
|
||||
|
||||
// 전체 페이지 수
|
||||
getTotalPages() {
|
||||
try {
|
||||
if (!Array.isArray(this.videos)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const videosPerPage = this.config.videosPerPage || this.videosPerPage || 6;
|
||||
if (videosPerPage <= 0) {
|
||||
this._handleError(new Error('videosPerPage가 0 이하입니다.'), 'getTotalPages');
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.ceil(this.videos.length / videosPerPage));
|
||||
} catch (error) {
|
||||
this._handleError(error, 'getTotalPages');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스 정리 (이벤트 리스너 제거)
|
||||
*/
|
||||
destroy() {
|
||||
try {
|
||||
// 이벤트 리스너 제거
|
||||
if (this.eventManager && this.listenerIds.length > 0) {
|
||||
this.listenerIds.forEach(({ element, id }) => {
|
||||
this.eventManager.off(element, id);
|
||||
});
|
||||
this.listenerIds = [];
|
||||
}
|
||||
|
||||
// 참조 정리
|
||||
this.config = null;
|
||||
this.videos = [];
|
||||
this.currentPage = 0;
|
||||
} catch (error) {
|
||||
this._handleError(error, 'destroy');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user