Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* 학습 페이지 초기화 및 관리
|
||||
* 공통 모듈 활용 (ErrorHandler, DOMUtils, EventManager, Utils)
|
||||
*/
|
||||
class LearningApp {
|
||||
constructor(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 {
|
||||
// HTML data 속성에서 설정 읽기
|
||||
this._loadSettingsFromHTML();
|
||||
|
||||
// 의존성 전달을 위한 객체 생성
|
||||
const commonDependencies = {
|
||||
domUtils: this.domUtils,
|
||||
errorHandler: this.errorHandler,
|
||||
eventManager: this.eventManager,
|
||||
utils: this.utils,
|
||||
animationUtils: this.animationUtils
|
||||
};
|
||||
|
||||
// GaugeManager 초기화 (의존성 주입)
|
||||
this.gauge = new GaugeManager(commonDependencies);
|
||||
|
||||
// MarkerManager 초기화
|
||||
this.markerManager = new MarkerManager(this.gauge, LEARNING_CONFIG);
|
||||
|
||||
// ChapterCardManager 초기화 (의존성 주입)
|
||||
this.chapterCardManager = new ChapterCardManager(
|
||||
LEARNING_CONFIG,
|
||||
this.gauge,
|
||||
commonDependencies
|
||||
);
|
||||
|
||||
// ProgressIndicator 초기화
|
||||
this.progressIndicator = new ProgressIndicator(
|
||||
LEARNING_CONFIG,
|
||||
this.gauge,
|
||||
this.markerManager
|
||||
);
|
||||
|
||||
// VideoModal은 선택 의존성으로 처리하여 모달 스크립트 문제 시에도
|
||||
// 경로/마커/챕터 카드는 정상 렌더링되도록 한다.
|
||||
this.modal = null;
|
||||
if (typeof VideoModal !== 'undefined') {
|
||||
this.modal = new VideoModal(LEARNING_CONFIG, this.markerManager);
|
||||
|
||||
if (this.markerManager && typeof this.markerManager.setModalInstance === 'function') {
|
||||
this.markerManager.setModalInstance(this.modal);
|
||||
}
|
||||
if (this.chapterCardManager && typeof this.chapterCardManager.setModalInstance === 'function') {
|
||||
this.chapterCardManager.setModalInstance(this.modal);
|
||||
}
|
||||
} else {
|
||||
console.warn('[LearningApp] VideoModal이 없어 모달 기능은 비활성화됩니다.');
|
||||
this.modal = this._createFallbackModalHandler();
|
||||
|
||||
if (this.markerManager && typeof this.markerManager.setModalInstance === 'function') {
|
||||
this.markerManager.setModalInstance(this.modal);
|
||||
}
|
||||
if (this.chapterCardManager && typeof this.chapterCardManager.setModalInstance === 'function') {
|
||||
this.chapterCardManager.setModalInstance(this.modal);
|
||||
}
|
||||
}
|
||||
|
||||
this.init();
|
||||
} catch (error) {
|
||||
this._handleError(error, 'LearningApp.constructor');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 처리 헬퍼
|
||||
* @private
|
||||
*/
|
||||
_handleError(error, context, additionalInfo = {}) {
|
||||
if (this.errorHandler) {
|
||||
this.errorHandler.handle(error, {
|
||||
context: `LearningApp.${context}`,
|
||||
component: 'LearningApp',
|
||||
...additionalInfo
|
||||
}, false);
|
||||
} else {
|
||||
console.error(`[LearningApp] ${context}:`, error, additionalInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML data 속성에서 설정 읽기
|
||||
* @private
|
||||
*/
|
||||
_loadSettingsFromHTML() {
|
||||
try {
|
||||
const learningGauge = this.domUtils?.$(".lessons-gauge") || document.querySelector(".lessons-gauge");
|
||||
if (!learningGauge) {
|
||||
console.warn("[LearningApp] .lessons-gauge 요소를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// data-allow-disabled-click
|
||||
const allowDisabledClick = learningGauge.dataset.allowDisabledClick;
|
||||
if (allowDisabledClick !== undefined) {
|
||||
LEARNING_CONFIG.settings.allowDisabledClick =
|
||||
allowDisabledClick === "true";
|
||||
}
|
||||
|
||||
// data-show-disabled-alert
|
||||
const showDisabledAlert = learningGauge.dataset.showDisabledAlert;
|
||||
if (showDisabledAlert !== undefined) {
|
||||
LEARNING_CONFIG.settings.showDisabledAlert = showDisabledAlert === "true";
|
||||
}
|
||||
|
||||
// data-disabled-click-message
|
||||
const disabledClickMessage = learningGauge.dataset.disabledClickMessage;
|
||||
if (disabledClickMessage) {
|
||||
LEARNING_CONFIG.settings.disabledClickMessage = disabledClickMessage;
|
||||
}
|
||||
|
||||
console.log("[LearningApp] HTML 설정 로드 완료:", LEARNING_CONFIG.settings);
|
||||
} catch (error) {
|
||||
this._handleError(error, '_loadSettingsFromHTML');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기화
|
||||
*/
|
||||
init() {
|
||||
try {
|
||||
const initHandler = () => {
|
||||
try {
|
||||
this._initializeComponents();
|
||||
} catch (error) {
|
||||
this._handleError(error, 'init.initHandler');
|
||||
}
|
||||
};
|
||||
|
||||
// DOMContentLoaded 이벤트 처리
|
||||
if (document.readyState === 'loading') {
|
||||
if (this.eventManager) {
|
||||
const listenerId = this.eventManager.on(window, "DOMContentLoaded", initHandler);
|
||||
this.listenerIds.push({ element: window, id: listenerId, type: 'DOMContentLoaded' });
|
||||
} else {
|
||||
window.addEventListener("DOMContentLoaded", initHandler);
|
||||
}
|
||||
} else {
|
||||
// 이미 로드된 경우 즉시 실행
|
||||
initHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, 'init');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 초기화
|
||||
* @private
|
||||
*/
|
||||
_initializeComponents() {
|
||||
try {
|
||||
const useMarkers = LEARNING_CONFIG?.settings?.useMarkers !== false;
|
||||
|
||||
// 마커 생성 (설정에서 비활성화 가능)
|
||||
if (useMarkers && this.markerManager && typeof this.markerManager.createMarkers === 'function') {
|
||||
this.markerManager.createMarkers();
|
||||
} else if (!useMarkers) {
|
||||
console.log("[LearningApp] 마커 UI 비활성화: createMarkers 생략");
|
||||
} else {
|
||||
console.warn("[LearningApp] markerManager.createMarkers를 호출할 수 없습니다.");
|
||||
}
|
||||
|
||||
// 챕터 카드 생성
|
||||
if (this.chapterCardManager && typeof this.chapterCardManager.createChapterCards === 'function') {
|
||||
this.chapterCardManager.createChapterCards();
|
||||
} else {
|
||||
console.warn("[LearningApp] chapterCardManager.createChapterCards를 호출할 수 없습니다.");
|
||||
}
|
||||
|
||||
// 진행률 표시 생성
|
||||
if (this.progressIndicator && typeof this.progressIndicator.createIndicator === 'function') {
|
||||
this.progressIndicator.createIndicator();
|
||||
} else {
|
||||
console.warn("[LearningApp] progressIndicator.createIndicator를 호출할 수 없습니다.");
|
||||
}
|
||||
|
||||
// 초기 진행률 설정
|
||||
this._initializeProgress();
|
||||
} catch (error) {
|
||||
this._handleError(error, '_initializeComponents');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기 진행률 설정
|
||||
* @private
|
||||
*/
|
||||
_initializeProgress() {
|
||||
try {
|
||||
if (!this.gauge || !this.markerManager) {
|
||||
console.warn("[LearningApp] gauge 또는 markerManager가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 초기 진행률 설정 (마커의 실제 DOM 위치 기반)
|
||||
const targetMarkerConfig = this.gauge.calculateInitialProgress(
|
||||
this.markerManager.allMarkers,
|
||||
LEARNING_CONFIG
|
||||
);
|
||||
|
||||
if (!targetMarkerConfig) {
|
||||
console.warn("[LearningApp] 타겟 마커 설정을 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 실제 강의만 카운트 (챕터 제외)
|
||||
const learningMarkers = (this.markerManager.allMarkers || []).filter(
|
||||
(m) => m && m.isLearningContent !== false
|
||||
);
|
||||
const completedLearningCount = learningMarkers.filter(
|
||||
(m) => m && m.completed === true
|
||||
).length;
|
||||
|
||||
// 마커의 실제 DOM 위치를 찾아서 가장 가까운 pathPercent 계산
|
||||
let initialPathPercent = 0;
|
||||
|
||||
// 100% 완료 시 게이지바를 100%로 설정
|
||||
if (completedLearningCount >= learningMarkers.length) {
|
||||
initialPathPercent = 1.0; // 100% 완료
|
||||
console.log(
|
||||
`[LearningApp] 초기 진행률: 모든 학습 완료, 게이지바 100%로 설정`
|
||||
);
|
||||
} else if (targetMarkerConfig) {
|
||||
// 타겟 마커 찾기 (pathPercent와 label로 비교)
|
||||
const targetMarker = (this.markerManager.markers || []).find(
|
||||
(m) =>
|
||||
m &&
|
||||
m.config &&
|
||||
m.config.pathPercent === targetMarkerConfig.pathPercent &&
|
||||
m.config.label === targetMarkerConfig.label
|
||||
);
|
||||
|
||||
if (targetMarker && targetMarker.element) {
|
||||
// gaugePercent가 있으면 우선 사용, 없으면 마커의 실제 DOM 위치 기반으로 계산
|
||||
if (targetMarkerConfig.gaugePercent !== undefined) {
|
||||
initialPathPercent = targetMarkerConfig.gaugePercent;
|
||||
console.log(
|
||||
`[LearningApp] 초기 진행률: gaugePercent 사용: ${(initialPathPercent * 100).toFixed(1)}%`
|
||||
);
|
||||
} else {
|
||||
// 마커의 실제 DOM 위치 가져오기
|
||||
const markerLeft = parseFloat(targetMarker.element.style.left) || 0;
|
||||
const markerTop = parseFloat(targetMarker.element.style.top) || 0;
|
||||
|
||||
// maskPath에서 마커 위치에 가장 가까운 지점 찾기
|
||||
const closestPercent = this.gauge.findClosestPathPercent(markerLeft, markerTop);
|
||||
if (closestPercent !== null && closestPercent !== undefined) {
|
||||
initialPathPercent = closestPercent;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[LearningApp] 초기 진행률: 마커 실제 위치 (${markerLeft.toFixed(2)}%, ${markerTop.toFixed(2)}%) → pathPercent: ${initialPathPercent.toFixed(4)}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 마커를 찾을 수 없는 경우 gaugePercent 우선 사용, 없으면 pathPercent 사용
|
||||
initialPathPercent = targetMarkerConfig.gaugePercent !== undefined
|
||||
? targetMarkerConfig.gaugePercent
|
||||
: (targetMarkerConfig.pathPercent || 0);
|
||||
console.log(
|
||||
`[LearningApp] 초기 진행률: 마커를 찾을 수 없음, ${targetMarkerConfig.gaugePercent !== undefined ? 'gaugePercent' : 'pathPercent'} 직접 사용: ${(initialPathPercent * 100).toFixed(1)}%`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 마커 실제 위치에 가장 가까운 pathPercent를 사용하여 채움 (초기 로딩 시 애니메이션 없음)
|
||||
if (this.gauge && typeof this.gauge.setProgress === 'function') {
|
||||
this.gauge.setProgress(initialPathPercent, true, false);
|
||||
}
|
||||
|
||||
// 진행률 표시 업데이트
|
||||
if (this.progressIndicator && typeof this.progressIndicator.updateProgress === 'function') {
|
||||
this.progressIndicator.updateProgress(this.markerManager.allMarkers);
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, '_initializeProgress');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 학습 완료 후 챕터 카드 및 진행률 표시 업데이트
|
||||
*/
|
||||
updateChapterCards() {
|
||||
try {
|
||||
if (this.chapterCardManager && typeof this.chapterCardManager.updateChapterCards === 'function') {
|
||||
this.chapterCardManager.updateChapterCards();
|
||||
}
|
||||
|
||||
if (this.progressIndicator && typeof this.progressIndicator.updateProgress === 'function' && this.markerManager) {
|
||||
this.progressIndicator.updateProgress(this.markerManager.allMarkers);
|
||||
}
|
||||
} catch (error) {
|
||||
this._handleError(error, 'updateChapterCards');
|
||||
}
|
||||
}
|
||||
|
||||
_createFallbackModalHandler() {
|
||||
const app = this;
|
||||
let modalEl = null;
|
||||
|
||||
const escapeHtml = (value) => String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
const toEmbedUrl = (rawUrl) => {
|
||||
const raw = String(rawUrl ?? '').trim();
|
||||
const match = raw.match(/(?:v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{11})/);
|
||||
const id = match ? match[1] : raw;
|
||||
return `https://www.youtube.com/embed/${id}?rel=0&modestbranding=1`;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
if (!modalEl) return;
|
||||
modalEl.remove();
|
||||
modalEl = null;
|
||||
if (typeof bodyUnlock === 'function') bodyUnlock();
|
||||
};
|
||||
|
||||
const renderChapter = (chapter, lessonIndex) => {
|
||||
if (!modalEl || !chapter || !Array.isArray(chapter.lessons)) return;
|
||||
|
||||
const safeIndex = Math.max(0, Math.min(chapter.lessons.length - 1, lessonIndex));
|
||||
const lesson = chapter.lessons[safeIndex] || {};
|
||||
const title = lesson.title || chapter.name || '학습 영상';
|
||||
const iframe = modalEl.querySelector('#videoFrame');
|
||||
const heading = modalEl.querySelector('.video-info h3');
|
||||
const subTitle = modalEl.querySelector('.video-header .sub-txt');
|
||||
|
||||
if (iframe) iframe.src = toEmbedUrl(lesson.url);
|
||||
if (heading) heading.textContent = chapter.name || '';
|
||||
if (subTitle) subTitle.textContent = title;
|
||||
|
||||
const total = chapter.lessons.length;
|
||||
const percent = total > 0 ? Math.round(((safeIndex + 1) / total) * 100) : 0;
|
||||
const gaugeFill = modalEl.querySelector('#gaugeFill');
|
||||
const gaugeLabel = modalEl.querySelector('#currentValue em');
|
||||
const stepLabel = modalEl.querySelector('.gauge-labels .label em');
|
||||
if (gaugeFill) gaugeFill.style.width = `${percent}%`;
|
||||
if (gaugeLabel) gaugeLabel.textContent = String(percent);
|
||||
if (stepLabel) stepLabel.textContent = String(safeIndex + 1);
|
||||
|
||||
const listWrap = modalEl.querySelector('.learning-list');
|
||||
if (!listWrap) return;
|
||||
|
||||
listWrap.innerHTML = chapter.lessons.map((item, idx) => {
|
||||
const activeClass = idx === safeIndex ? 'active' : (item.completed ? 'complet' : '');
|
||||
const stateText = idx === safeIndex ? '학습중' : (item.completed ? '학습완료' : '미진행');
|
||||
return `
|
||||
<li class="${activeClass}">
|
||||
<a href="#" class="list" data-lesson-index="${idx}">
|
||||
<span class="seq">${idx + 1}차시</span>
|
||||
<div class="learning-box">
|
||||
<div class="txt-box">
|
||||
<div class="title">${escapeHtml(item.title || chapter.name || '')}</div>
|
||||
<span class="state">${stateText}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
listWrap.querySelectorAll('a.list[data-lesson-index]').forEach((a) => {
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const next = Number.parseInt(a.getAttribute('data-lesson-index') || '0', 10);
|
||||
renderChapter(chapter, Number.isFinite(next) ? next : 0);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
async loadChapter(chapter, chapterIndex, initialLessonIndex) {
|
||||
if (!chapter || !Array.isArray(chapter.lessons) || chapter.lessons.length === 0) return;
|
||||
|
||||
const chapterInfo = LEARNING_CONFIG.getChapterByGlobalIndex
|
||||
? LEARNING_CONFIG.getChapterByGlobalIndex(initialLessonIndex)
|
||||
: null;
|
||||
const lessonIndex = chapterInfo && typeof chapterInfo.lessonIndex === 'number' && chapterInfo.lessonIndex >= 0
|
||||
? chapterInfo.lessonIndex
|
||||
: 0;
|
||||
|
||||
closeModal();
|
||||
modalEl = document.createElement('div');
|
||||
modalEl.innerHTML = `
|
||||
<div class="modal video on" style="display:block;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="video-contents">
|
||||
<div class="video-area"><div class="video-box"><iframe id="videoFrame" width="100%" height="100%" src="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></div>
|
||||
<div class="video-info"><div class="tit-box"><div class="meta"><span>법정교육</span><em></em></div><h3></h3></div></div>
|
||||
</div>
|
||||
<div class="video-side step">
|
||||
<div class="video-header">
|
||||
<div class="tit-box"><h5 class="tit">현재강의</h5><p class="sub-txt"></p></div>
|
||||
<div class="gauge-container"><div class="gauge-bar"><div class="gauge-fill" id="gaugeFill" style="width:0%"></div></div><div class="gauge-labels"><span class="label"><em>1</em>/${chapter.lessons.length} 강</span><span class="label current" id="currentValue">진도율 <em>0</em>%</span></div></div>
|
||||
<span class="close" role="button" tabindex="0">×</span>
|
||||
</div>
|
||||
<div class="video-list"><h5 class="tit">학습목차</h5><ul class="learning-list"></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rootModal = modalEl.firstElementChild;
|
||||
if (!rootModal) return;
|
||||
document.body.appendChild(rootModal);
|
||||
modalEl = rootModal;
|
||||
|
||||
const closeBtn = modalEl.querySelector('.close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
modalEl.addEventListener('click', (e) => {
|
||||
if (e.target === modalEl) closeModal();
|
||||
});
|
||||
|
||||
if (typeof bodyLock === 'function') bodyLock();
|
||||
renderChapter(chapter, lessonIndex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스 정리 (이벤트 리스너 제거)
|
||||
*/
|
||||
destroy() {
|
||||
try {
|
||||
// 이벤트 리스너 제거
|
||||
if (this.eventManager && this.listenerIds.length > 0) {
|
||||
this.listenerIds.forEach(({ element, id }) => {
|
||||
this.eventManager.off(element, id);
|
||||
});
|
||||
this.listenerIds = [];
|
||||
}
|
||||
|
||||
// 컴포넌트 정리
|
||||
if (this.chapterCardManager && typeof this.chapterCardManager.destroy === 'function') {
|
||||
this.chapterCardManager.destroy();
|
||||
}
|
||||
|
||||
// 참조 정리
|
||||
this.gauge = null;
|
||||
this.markerManager = null;
|
||||
this.chapterCardManager = null;
|
||||
this.progressIndicator = null;
|
||||
this.modal = null;
|
||||
} catch (error) {
|
||||
this._handleError(error, 'destroy');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 학습 앱 초기화 함수 (에러 처리 포함)
|
||||
* @param {Object} dependencies - 의존성 객체
|
||||
*/
|
||||
function initLearningApp(dependencies = {}) {
|
||||
try {
|
||||
// 의존성 주입 (없으면 자동 감지)
|
||||
const finalDependencies = {
|
||||
domUtils: dependencies.domUtils || (typeof DOMUtils !== 'undefined' ? DOMUtils : null),
|
||||
errorHandler: dependencies.errorHandler || (typeof ErrorHandler !== 'undefined' ? ErrorHandler : null),
|
||||
eventManager: dependencies.eventManager || (typeof eventManager !== 'undefined' ? eventManager : null),
|
||||
utils: dependencies.utils || (typeof Utils !== 'undefined' ? Utils : null),
|
||||
animationUtils: dependencies.animationUtils || (typeof AnimationUtils !== 'undefined' ? AnimationUtils : null),
|
||||
...dependencies
|
||||
};
|
||||
|
||||
// LEARNING_CONFIG 유효성 검증
|
||||
if (typeof LEARNING_CONFIG === 'undefined') {
|
||||
const error = new Error('LEARNING_CONFIG가 정의되지 않았습니다.');
|
||||
if (finalDependencies.errorHandler) {
|
||||
finalDependencies.errorHandler.handle(error, {
|
||||
context: 'initLearningApp'
|
||||
}, true); // 사용자에게 표시
|
||||
} else {
|
||||
console.error('[LearningApp]', error);
|
||||
alert('학습 설정을 불러올 수 없습니다.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// LearningApp 인스턴스 생성
|
||||
window.learningApp = new LearningApp(finalDependencies);
|
||||
|
||||
if (!window.learningApp) {
|
||||
throw new Error('LearningApp 인스턴스 생성 실패');
|
||||
}
|
||||
|
||||
console.log('[LearningApp] 초기화 완료');
|
||||
} catch (error) {
|
||||
const errorHandler = dependencies.errorHandler || (typeof ErrorHandler !== 'undefined' ? ErrorHandler : null);
|
||||
if (errorHandler) {
|
||||
errorHandler.handle(error, {
|
||||
context: 'initLearningApp'
|
||||
}, true); // 사용자에게 표시
|
||||
} else {
|
||||
console.error('[LearningApp] 초기화 에러:', error);
|
||||
alert('학습 앱 초기화 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 초기화 실행
|
||||
if (document.readyState === 'loading') {
|
||||
// DOMContentLoaded는 defer 스크립트가 모두 로드된 후에 발생
|
||||
if (typeof eventManager !== 'undefined' && eventManager) {
|
||||
eventManager.on(document, 'DOMContentLoaded', () => initLearningApp());
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => initLearningApp());
|
||||
}
|
||||
} else {
|
||||
// 이미 로드된 경우 즉시 시도
|
||||
initLearningApp();
|
||||
}
|
||||
Reference in New Issue
Block a user