// ============================================ // 비디오 모달 관리 모듈 (Videomodalmanager.js) // ============================================ // // [역할] 비디오 카드 클릭 시 모달을 열고, YouTube 영상을 재생합니다. // // [파일명 참고] "Videomodalmanager"는 legacy 표기입니다. // 클래스명은 VideoModalManager (PascalCase)를 사용합니다. // // [의존성] VideoModalBase(부모), DOMUtils, EventManager, ErrorHandler, Utils // // [사용 예] index.html 등에서: // const modalManager = new VideoModalManager({ videos: [...] }); // modalManager.init(); // // ============================================ class VideoModalManager extends VideoModalBase { constructor(config, dependencies = {}) { // 입력값 유효성 검증 (super 호출 전에 가능한 작업만) if (!config || typeof config !== 'object') { config = {}; } // VideoModalBase에 전달할 config 준비 const baseConfig = { videos: config.videos || [], modalPath: config.modalPath || "./_modal/video.php", modalPathTemplate: config.modalPathTemplate || "./_modal/video-{type}.php", enableHeightAdjustment: config.enableHeightAdjustment !== false, enableCommentResizer: config.enableCommentResizer !== false, enableCommentBox: config.enableCommentBox !== false, ...config, }; // 부모 클래스 생성자 호출 (반드시 먼저 호출) super(baseConfig); // 의존성 주입 (폴백 포함) - super() 호출 후 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 = []; this._isInitialized = false; this._openVideoPromise = null; try { // 추가 초기화 작업 } catch (error) { this._handleError(error, 'constructor'); } } /** * 에러 처리 헬퍼 * @private */ _handleError(error, context, additionalInfo = {}) { if (this.errorHandler) { this.errorHandler.handle(error, { context: `VideoModalManager.${context}`, component: 'VideoModalManager', ...additionalInfo }, false); } else { console.error(`[VideoModalManager] ${context}:`, error, additionalInfo); } } // 초기화 init() { try { if (this._isInitialized) { return; } this.setupCardClickEvents(); this._isInitialized = true; } catch (error) { this._handleError(error, 'init'); } } async openVideo(videoIdOrData) { if (this._openVideoPromise) { return this._openVideoPromise; } this._openVideoPromise = super.openVideo(videoIdOrData) .finally(() => { this._openVideoPromise = null; }); return this._openVideoPromise; } // 카드 클릭 이벤트 설정 (DOMUtils, EventManager 활용) setupCardClickEvents() { try { const containers = Array.from(document.querySelectorAll('[data-video-cards-container="true"], .js-video-cards-container, #videoCardsContainer')); if (containers.length === 0) { console.warn("[VideoModalManager] videoCardsContainer를 찾을 수 없습니다."); return; } // VideoModalManager 인스턴스를 참조하기 위해 변수에 저장 const self = this; const clickHandler = function (e) { try { // 북마크 클릭은 카드 오픈 로직에서 제외해야 체크 토글/change 저장이 정상 동작한다. if (e.target && e.target.closest && e.target.closest('.bookmark')) { return; } const trigger = e.target && e.target.closest ? e.target.closest('.book-info-btn[data-video-id], .books-item[data-video-id], .card[data-video-id], [data-video-id]') : null; if (!trigger) { return; } e.preventDefault(); e.stopPropagation(); const videoIdAttr = trigger.getAttribute("data-video-id"); if (!videoIdAttr) { console.warn("[VideoModalManager] data-video-id 속성이 없습니다."); return; } const videoId = videoIdAttr; if (!videoId) { self._handleError(new Error(`유효하지 않은 videoId: ${videoIdAttr}`), 'setupCardClickEvents.clickHandler'); return; } self.openVideo(videoId); } catch (error) { self._handleError(error, 'setupCardClickEvents.clickHandler'); } }; containers.forEach((container) => { const nativeHandler = function (e) { clickHandler(e); }; container.addEventListener("click", nativeHandler); self.listenerIds.push({ element: container, handler: nativeHandler, type: 'native' }); }); } catch (error) { this._handleError(error, 'setupCardClickEvents'); } } // 기존 메서드 호환성을 위한 래퍼 async loadVideoModal(videoId) { try { // 입력값 유효성 검증 if (!videoId || (typeof videoId !== 'number' && typeof videoId !== 'string')) { this._handleError(new Error(`유효하지 않은 videoId: ${videoId}`), 'loadVideoModal'); return null; } const id = videoId; if (!id) { this._handleError(new Error(`유효하지 않은 videoId: ${videoId}`), 'loadVideoModal'); return null; } return await this.openVideo(id); } catch (error) { this._handleError(error, 'loadVideoModal', { videoId }); return null; } } // 기존 코드 호환성을 위한 래퍼 메서드들 get currentModal() { try { return this.currentModalElement; } catch (error) { this._handleError(error, 'currentModal.get'); return null; } } set currentModal(value) { try { this.currentModalElement = value; } catch (error) { this._handleError(error, 'currentModal.set', { value }); } } // 기존 메서드 호환성 유지 (VideoModalBase의 메서드 사용) adjustVideoListHeight() { try { if (this.currentModalElement) { super.adjustVideoListHeight(this.currentModalElement); } } catch (error) { this._handleError(error, 'adjustVideoListHeight'); } } setupCommentResizer() { try { if (this.currentModalElement) { super.setupCommentResizer(this.currentModalElement); } } catch (error) { this._handleError(error, 'setupCommentResizer'); } } setupCommentBox() { try { if (this.currentModalElement) { super.setupCommentBox(this.currentModalElement); } } catch (error) { this._handleError(error, 'setupCommentBox'); } } showCommentSection() { try { if (this.currentModalElement) { super.showCommentSection(this.currentModalElement); } } catch (error) { this._handleError(error, 'showCommentSection'); } } adjustCommentOnlyLayout() { try { if (this.currentModalElement) { super.adjustCommentOnlyLayout(this.currentModalElement); } } catch (error) { this._handleError(error, 'adjustCommentOnlyLayout'); } } setupEssentialLayout() { try { if (this.currentModalElement) { super.setupEssentialLayout(this.currentModalElement); } } catch (error) { this._handleError(error, 'setupEssentialLayout'); } } setupLearningLayout() { try { if (this.currentModalElement) { super.setupLearningLayout(this.currentModalElement); } } catch (error) { this._handleError(error, 'setupLearningLayout'); } } initializeHeightAdjustment() { try { if (this.currentModalElement) { super.initializeHeightAdjustment(this.currentModalElement); } } catch (error) { this._handleError(error, 'initializeHeightAdjustment'); } } setupResizeObserver() { try { if (this.currentModalElement) { super.setupResizeObserver(this.currentModalElement); } } catch (error) { this._handleError(error, 'setupResizeObserver'); } } setupMutationObserver() { try { if (this.currentModalElement) { super.setupMutationObserver(this.currentModalElement); } } catch (error) { this._handleError(error, 'setupMutationObserver'); } } async waitForImagesAndAdjust() { try { if (this.currentModalElement) { await super.waitForImagesAndAdjust(this.currentModalElement); } } catch (error) { this._handleError(error, 'waitForImagesAndAdjust'); } } destroyModal() { try { // 이벤트 리스너 제거 if (this.eventManager && this.listenerIds.length > 0) { this.listenerIds.forEach(({ element, id, type }) => { if (type === 'delegate') { this.eventManager.undelegate(element, id); } else if (type === 'native') { element.removeEventListener('click', id || arguments[0]); } else { this.eventManager.off(element, id); } }); this.listenerIds = []; } else if (this.listenerIds.length > 0) { this.listenerIds.forEach(({ element, handler, type }) => { if (type === 'native' && element && handler) { element.removeEventListener('click', handler); } }); this.listenerIds = []; } // 부모 클래스의 destroy 호출 if (super.destroy && typeof super.destroy === 'function') { super.destroy(); } else if (this.destroy && typeof this.destroy === 'function') { this.destroy(); } this._isInitialized = false; } catch (error) { this._handleError(error, 'destroyModal'); } } } if (typeof window !== 'undefined') { window.VideoModalManager = VideoModalManager; } if (typeof globalThis !== 'undefined') { globalThis.VideoModalManager = VideoModalManager; } if (typeof module !== 'undefined' && module.exports) { module.exports = { VideoModalManager }; }