/** * 학습 경로 설정 * 공통 모듈 활용 (ErrorHandler, Utils, ConfigManager) */ const LEARNING_CONFIG = { // 마커 설정 - 챕터별로 그룹화 chapters: [ { id: 1, code: "CA200C01", name: "개인정보보호", type: "chapter", pathPercent: 0.108, pathPercentMo: 0.108, // PC와 동일 순서 gaugePercent: 0.108, gaugePercentMo: 0.108, url: "ddILV5cbdQo", completed: false, // 하위 lessons가 모두 완료되면 자동으로 true lessons: [ { pathPercent: 0.137, pathPercentMo: 0.137, gaugePercent: 0.137, gaugePercentMo: 0.137, type: "normal", label: "개인정보보호 1", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.159, pathPercentMo: 0.159, gaugePercent: 0.156, gaugePercentMo: 0.156, type: "normal", label: "개인정보보호 2", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.182, pathPercentMo: 0.182, gaugePercent: 0.178, gaugePercentMo: 0.178, type: "normal", label: "개인정보보호 3", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.205, pathPercentMo: 0.205, gaugePercent: 0.202, gaugePercentMo: 0.202, type: "normal", label: "개인정보보호 4", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.228, pathPercentMo: 0.228, gaugePercent: 0.226, gaugePercentMo: 0.226, type: "normal", label: "개인정보보호 5", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.25, pathPercentMo: 0.25, gaugePercent: 0.246, gaugePercentMo: 0.246, type: "normal", label: "개인정보보호 6", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.272, pathPercentMo: 0.272, gaugePercent: 0.268, gaugePercentMo: 0.268, type: "normal", label: "개인정보보호 7", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.298, pathPercentMo: 0.298, gaugePercent: 0.296, gaugePercentMo: 0.296, type: "normal", label: "개인정보보호 8", url: "ddILV5cbdQo", completed: false, }, ], }, { id: 2, code: "CA200C02", name: "직장내 괴롭힘 예방", type: "chapter", pathPercent: 0.325, pathPercentMo: 0.325, gaugePercent: 0.325, gaugePercentMo: 0.325, url: "ddILV5cbdQo", completed: false, lessons: [ { pathPercent: 0.367, pathPercentMo: 0.367, gaugePercent: 0.358, gaugePercentMo: 0.358, type: "normal", label: "직장내 괴롭힘 예방 1", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.41, pathPercentMo: 0.41, gaugePercent: 0.40, gaugePercentMo: 0.40, type: "normal", label: "직장내 괴롭힘 예방 2", url: "ddILV5cbdQo", completed: false, }, ], }, { id: 3, code: "CA200C03", name: "성희롱 예방 교육", type: "chapter", pathPercent: 0.442, pathPercentMo: 0.442, gaugePercent: 0.442, gaugePercentMo: 0.442, url: "ddILV5cbdQo", completed: false, lessons: [ { pathPercent: 0.486, pathPercentMo: 0.486, gaugePercent: 0.476, gaugePercentMo: 0.476, type: "normal", label: "성희롱 예방 교육 1", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.531, pathPercentMo: 0.531, gaugePercent: 0.526, gaugePercentMo: 0.526, type: "normal", label: "성희롱 예방 교육 2", url: "ddILV5cbdQo", completed: false, }, ], }, { id: 4, code: "CA200C04", name: "산업안전 보건", type: "chapter", pathPercent: 0.555, pathPercentMo: 0.555, gaugePercent: 0.555, gaugePercentMo: 0.555, url: "ddILV5cbdQo", completed: false, lessons: [ { pathPercent: 0.585, pathPercentMo: 0.585, gaugePercent: 0.582, gaugePercentMo: 0.582, type: "normal", label: "산업안전 보건 1", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.635, pathPercentMo: 0.635, gaugePercent: 0.632, gaugePercentMo: 0.632, type: "normal", label: "산업안전 보건 2", url: "ddILV5cbdQo", completed: false, }, ], }, { id: 5, code: "CA200C05", name: "장애인 인식 개선", type: "chapter", pathPercent: 0.67, pathPercentMo: 0.67, gaugePercent: 0.67, gaugePercentMo: 0.67, url: "ddILV5cbdQo", completed: false, lessons: [ { pathPercent: 0.69, pathPercentMo: 0.69, gaugePercent: 0.686, gaugePercentMo: 0.686, type: "normal", label: "장애인 인식 개선 1", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.718, pathPercentMo: 0.718, gaugePercent: 0.71, gaugePercentMo: 0.71, type: "normal", label: "장애인 인식 개선 2", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.736, pathPercentMo: 0.736, gaugePercent: 0.728, gaugePercentMo: 0.728, type: "normal", label: "장애인 인식 개선 3", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.785, pathPercentMo: 0.785, gaugePercent: 0.778, gaugePercentMo: 0.778, type: "normal", label: "장애인 인식 개선 4", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.82, pathPercentMo: 0.82, gaugePercent: 0.812, gaugePercentMo: 0.812, type: "normal", label: "장애인 인식 개선 5", url: "ddILV5cbdQo", completed: false, }, { pathPercent: 0.86, pathPercentMo: 0.86, gaugePercent: 0.85, gaugePercentMo: 0.85, type: "normal", label: "장애인 인식 개선 6", url: "ddILV5cbdQo", completed: false, }, ], }, ], // 평균 학습량 설정 (전체 학습 항목 대비 %) averageProgress: { threshold: 60, // 평균 학습량: 전체의 60% }, // 마커 이미지 경로 markerImages: { normal: { base: "/img/learning/mark_base.png", current: "/img/learning/mark_current.png", completed: "/img/learning/mark_completed.png", }, chapter: { base: "/img/learning/mark_chapter_base.png", current: "/img/learning/mark_chapter_current.png", completed: "/img/learning/mark_chapter_completed.png", }, }, // 상태 이미지 경로 stateImages: { below: "/img/learning/img_state_01.svg", // 평균 이하 average: "/img/learning/img_state_02.svg", // 평균 above: "/img/learning/img_state_03.svg", // 평균 이상 }, // 모달 경로 modalPath: "./_modal/video-learning.php", // 비활성 마커 클릭 설정 settings: { allowDisabledClick: true, // true: 비활성 마커도 클릭 가능, false: 비활성 마커 클릭 불가 disabledClickMessage: "이전 학습을 먼저 완료해주세요.", // 비활성 마커 클릭 시 메시지 showDisabledAlert: false, // true: 알림 표시, false: 콘솔 로그만 showStateIndicator: { pc: true, // PC에서 상태 인디케이터 표시 mo: false, // 모바일에서도 상태 인디케이터 표시 (PC와 동일) }, }, /** * 챕터의 완료 상태 자동 업데이트 * 하위 lessons가 모두 완료되면 챕터도 completed = true로 변경 */ updateChapterCompletionStatus() { try { if (!this.chapters || !Array.isArray(this.chapters)) { this._handleError(new Error('chapters가 배열이 아닙니다.'), 'updateChapterCompletionStatus'); return; } this.chapters.forEach((chapter, index) => { try { if (!chapter || !chapter.lessons || !Array.isArray(chapter.lessons)) { console.warn(`[LEARNING_CONFIG] 챕터 ${index}의 lessons가 유효하지 않습니다.`); return; } const allLessonsCompleted = chapter.lessons.every( (lesson) => lesson && lesson.completed === true ); chapter.completed = allLessonsCompleted; } catch (error) { this._handleError(error, 'updateChapterCompletionStatus.chapter', { chapterIndex: index }); } }); } catch (error) { this._handleError(error, 'updateChapterCompletionStatus'); } }, /** * 전체 마커 배열 반환 (챕터 + lessons flat) * @returns {Array} */ getAllMarkers() { try { // 챕터 완료 상태 업데이트 this.updateChapterCompletionStatus(); if (!this.chapters || !Array.isArray(this.chapters)) { this._handleError(new Error('chapters가 배열이 아닙니다.'), 'getAllMarkers'); return []; } const markers = []; this.chapters.forEach((chapter, chapterIndex) => { try { if (!chapter) { console.warn(`[LEARNING_CONFIG] 챕터 ${chapterIndex}가 null입니다.`); return; } // 챕터 자체를 마커로 추가 (시작점 표시용, 클릭 불가) markers.push({ pathPercent: chapter.pathPercent || 0, pathPercentMo: chapter.pathPercentMo, gaugePercent: chapter.gaugePercent !== undefined ? chapter.gaugePercent : (chapter.pathPercent || 0), gaugePercentMo: chapter.gaugePercentMo, type: chapter.type || 'chapter', label: chapter.name || `챕터 ${chapterIndex + 1}`, url: chapter.url || '', completed: chapter.completed === true, chapterId: chapter.id || chapterIndex + 1, isChapterMarker: true, isLearningContent: false, // 강의 아님 isClickable: false, // 클릭 불가 }); // 하위 lessons 추가 (실제 강의) if (chapter.lessons && Array.isArray(chapter.lessons)) { chapter.lessons.forEach((lesson, lessonIndex) => { try { if (!lesson) { console.warn(`[LEARNING_CONFIG] 챕터 ${chapterIndex}의 레슨 ${lessonIndex}가 null입니다.`); return; } markers.push({ pathPercent: lesson.pathPercent || 0, pathPercentMo: lesson.pathPercentMo, gaugePercent: lesson.gaugePercent !== undefined ? lesson.gaugePercent : (lesson.pathPercent || 0), gaugePercentMo: lesson.gaugePercentMo, type: lesson.type || 'normal', label: lesson.label || `레슨 ${lessonIndex + 1}`, url: lesson.url || '', content_id: lesson.content_id || '', watch_tm: Math.max(0, Number.parseInt(lesson.watch_tm ?? 0, 10) || 0), content_tm: Math.max(0, Number.parseInt(lesson.content_tm ?? 0, 10) || 0), all_tm: Math.max(0, Number.parseInt(lesson.all_tm ?? 0, 10) || 0), completed: lesson.completed === true, chapterId: chapter.id || chapterIndex + 1, isChapterMarker: false, isLearningContent: true, // 실제 강의 isClickable: true, // 클릭 가능 }); } catch (error) { this._handleError(error, 'getAllMarkers.lesson', { chapterIndex, lessonIndex }); } }); } } catch (error) { this._handleError(error, 'getAllMarkers.chapter', { chapterIndex }); } }); return markers; } catch (error) { this._handleError(error, 'getAllMarkers'); return []; } }, /** * 특정 인덱스의 챕터 정보 반환 * @param {number} globalIndex - 전체 마커 기준 인덱스 * @returns {Object|null} { chapterIndex, chapterData, lessonIndex, lessonData, isChapterMarker } */ getChapterByGlobalIndex(globalIndex) { try { if (typeof globalIndex !== 'number' || globalIndex < 0) { this._handleError(new Error(`유효하지 않은 globalIndex: ${globalIndex}`), 'getChapterByGlobalIndex'); return null; } const allMarkers = this.getAllMarkers(); if (!Array.isArray(allMarkers) || globalIndex >= allMarkers.length) { console.warn(`[LEARNING_CONFIG] globalIndex ${globalIndex}가 범위를 벗어났습니다. (총 ${allMarkers.length}개)`); return null; } const marker = allMarkers[globalIndex]; if (!marker) { console.warn(`[LEARNING_CONFIG] globalIndex ${globalIndex}의 마커를 찾을 수 없습니다.`); return null; } const chapterIndex = this.chapters.findIndex( (ch) => ch && ch.id === marker.chapterId ); if (chapterIndex === -1) { console.warn(`[LEARNING_CONFIG] chapterId ${marker.chapterId}에 해당하는 챕터를 찾을 수 없습니다.`); return null; } const chapter = this.chapters[chapterIndex]; if (!chapter) { console.warn(`[LEARNING_CONFIG] 챕터 인덱스 ${chapterIndex}의 데이터가 없습니다.`); return null; } if (marker.isChapterMarker) { // 챕터 마커인 경우 return { chapterIndex, chapterData: chapter, lessonIndex: -1, // 챕터 자체이므로 -1 lessonData: marker, isChapterMarker: true, }; } else { // 일반 레슨인 경우 if (!chapter.lessons || !Array.isArray(chapter.lessons)) { console.warn(`[LEARNING_CONFIG] 챕터 ${chapterIndex}의 lessons가 유효하지 않습니다.`); return null; } const lessonIndex = chapter.lessons.findIndex( (lesson) => lesson && lesson.pathPercent === marker.pathPercent ); if (lessonIndex === -1) { console.warn(`[LEARNING_CONFIG] pathPercent ${marker.pathPercent}에 해당하는 레슨을 찾을 수 없습니다.`); return null; } return { chapterIndex, chapterData: chapter, lessonIndex, lessonData: marker, isChapterMarker: false, }; } } catch (error) { this._handleError(error, 'getChapterByGlobalIndex', { globalIndex }); return null; } }, /** * 로컬 인덱스를 글로벌 인덱스로 변환 * @param {number} chapterIndex - 챕터 인덱스 * @param {number} lessonIndex - 챕터 내 학습 인덱스 (-1이면 챕터 자체) * @returns {number|null} */ toGlobalIndex(chapterIndex, lessonIndex) { try { if (typeof chapterIndex !== 'number' || chapterIndex < 0) { this._handleError(new Error(`유효하지 않은 chapterIndex: ${chapterIndex}`), 'toGlobalIndex'); return null; } if (typeof lessonIndex !== 'number') { this._handleError(new Error(`유효하지 않은 lessonIndex: ${lessonIndex}`), 'toGlobalIndex'); return null; } if (!this.chapters || !Array.isArray(this.chapters)) { this._handleError(new Error('chapters가 배열이 아닙니다.'), 'toGlobalIndex'); return null; } if (chapterIndex >= this.chapters.length) { console.warn(`[LEARNING_CONFIG] chapterIndex ${chapterIndex}가 범위를 벗어났습니다. (총 ${this.chapters.length}개)`); return null; } let globalIndex = 0; for (let i = 0; i < chapterIndex; i++) { const chapter = this.chapters[i]; if (!chapter) { console.warn(`[LEARNING_CONFIG] 챕터 인덱스 ${i}가 null입니다.`); continue; } globalIndex += 1; // 챕터 마커 if (chapter.lessons && Array.isArray(chapter.lessons)) { globalIndex += chapter.lessons.length; // 하위 lessons } } if (lessonIndex === -1) { // 챕터 마커 자체 return globalIndex; } else { // 하위 lesson const chapter = this.chapters[chapterIndex]; if (!chapter || !chapter.lessons || !Array.isArray(chapter.lessons)) { console.warn(`[LEARNING_CONFIG] 챕터 ${chapterIndex}의 lessons가 유효하지 않습니다.`); return null; } if (lessonIndex >= chapter.lessons.length) { console.warn(`[LEARNING_CONFIG] lessonIndex ${lessonIndex}가 범위를 벗어났습니다. (총 ${chapter.lessons.length}개)`); return null; } return globalIndex + 1 + lessonIndex; } } catch (error) { this._handleError(error, 'toGlobalIndex', { chapterIndex, lessonIndex }); return null; } }, /** * 에러 처리 헬퍼 * @private */ _handleError(error, context, additionalInfo = {}) { if (typeof ErrorHandler !== 'undefined' && ErrorHandler) { ErrorHandler.handle(error, { context: `LEARNING_CONFIG.${context}`, component: 'LEARNING_CONFIG', ...additionalInfo }, false); } else { console.error(`[LEARNING_CONFIG] ${context}:`, error, additionalInfo); } }, /** * 설정 유효성 검증 * @returns {boolean} */ validate() { try { if (!this.chapters || !Array.isArray(this.chapters)) { this._handleError(new Error('chapters가 배열이 아닙니다.'), 'validate'); return false; } let isValid = true; this.chapters.forEach((chapter, index) => { if (!chapter) { console.warn(`[LEARNING_CONFIG] 챕터 ${index}가 null입니다.`); isValid = false; return; } if (!chapter.id) { console.warn(`[LEARNING_CONFIG] 챕터 ${index}에 id가 없습니다.`); isValid = false; } if (!chapter.name) { console.warn(`[LEARNING_CONFIG] 챕터 ${index}에 name이 없습니다.`); isValid = false; } if (!chapter.lessons || !Array.isArray(chapter.lessons)) { console.warn(`[LEARNING_CONFIG] 챕터 ${index}의 lessons가 유효하지 않습니다.`); isValid = false; } else { chapter.lessons.forEach((lesson, lessonIndex) => { if (!lesson) { console.warn(`[LEARNING_CONFIG] 챕터 ${index}의 레슨 ${lessonIndex}가 null입니다.`); isValid = false; } else { if (!lesson.label) { console.warn(`[LEARNING_CONFIG] 챕터 ${index}의 레슨 ${lessonIndex}에 label이 없습니다.`); isValid = false; } if (typeof lesson.pathPercent !== 'number') { console.warn(`[LEARNING_CONFIG] 챕터 ${index}의 레슨 ${lessonIndex}에 pathPercent가 없거나 숫자가 아닙니다.`); isValid = false; } } }); } }); return isValid; } catch (error) { this._handleError(error, 'validate'); return false; } }, }; /** * HTML에서 내려준 learningChapterData를 코드 기준으로 반영 * - category_group(code) 기준 챕터 매핑 * - lesson title/url/completed 동기화 */ if (typeof window !== "undefined" && Array.isArray(window.learningChapterData)) { try { const serverChapters = window.learningChapterData; const chapterByCode = new Map( serverChapters .filter((ch) => ch && typeof ch === 'object' && ch.code) .map((ch) => [String(ch.code), ch]) ); LEARNING_CONFIG.chapters.forEach((chapter, chapterIndex) => { const serverChapter = chapterByCode.get(String(chapter.code || '')) || serverChapters.find((ch) => Number(ch?.id) === Number(chapter.id)); if (!serverChapter) { return; } if (serverChapter.name) { chapter.name = String(serverChapter.name); } const serverLessons = Array.isArray(serverChapter.lessons) ? serverChapter.lessons : []; if (serverLessons.length === 0) { chapter.completed = false; chapter.lessons.forEach((lesson) => { lesson.completed = false; }); return; } const applyCount = Math.min(chapter.lessons.length, serverLessons.length); for (let i = 0; i < applyCount; i++) { const localLesson = chapter.lessons[i]; const serverLesson = serverLessons[i] || {}; if (serverLesson.title) { localLesson.label = String(serverLesson.title); } if (serverLesson.url) { localLesson.url = String(serverLesson.url); } if (serverLesson.content_id) { localLesson.content_id = String(serverLesson.content_id); } if (serverLesson.watch_tm !== undefined) { localLesson.watch_tm = Math.max(0, Number.parseInt(serverLesson.watch_tm, 10) || 0); } if (serverLesson.content_tm !== undefined) { localLesson.content_tm = Math.max(0, Number.parseInt(serverLesson.content_tm, 10) || 0); } if (serverLesson.all_tm !== undefined) { localLesson.all_tm = Math.max(0, Number.parseInt(serverLesson.all_tm, 10) || 0); } if (serverLesson.completed !== undefined) { localLesson.completed = serverLesson.completed === true; } } for (let i = applyCount; i < chapter.lessons.length; i++) { chapter.lessons[i].completed = false; } chapter.completed = chapter.lessons.length > 0 && chapter.lessons.every((lesson) => lesson && lesson.completed === true); if (serverLessons.length !== chapter.lessons.length) { console.warn( `[LEARNING_CONFIG] 챕터 ${chapterIndex + 1}(${chapter.code}) 차시 수 불일치: local=${chapter.lessons.length}, server=${serverLessons.length}` ); } }); } catch (error) { if (typeof ErrorHandler !== 'undefined' && ErrorHandler) { ErrorHandler.handle(error, { context: 'LEARNING_CONFIG.applyLearningChapterData' }, false); } else { console.error('[LEARNING_CONFIG] learningChapterData 적용 에러:', error); } } } /** * HTML에서 설정된 learningConfigData를 LEARNING_CONFIG에 적용 * window.learningConfigData가 있으면 completed 상태를 업데이트 */ if (typeof window !== "undefined" && window.learningConfigData) { try { const configData = window.learningConfigData; if (!configData || typeof configData !== 'object') { console.warn('[LEARNING_CONFIG] learningConfigData가 유효하지 않습니다.'); } else { LEARNING_CONFIG.chapters.forEach((chapter, chapterIndex) => { try { if (!chapter || !chapter.id) { console.warn(`[LEARNING_CONFIG] 챕터 ${chapterIndex}가 유효하지 않습니다.`); return; } const chapterData = configData[chapter.code] ?? configData[chapter.id]; if (chapterData) { // 챕터 완료 상태 업데이트 if (chapterData.completed !== undefined) { chapter.completed = chapterData.completed === true; } // 레슨 완료 상태 업데이트 if (chapterData.lessons && Array.isArray(chapterData.lessons)) { if (!chapter.lessons || !Array.isArray(chapter.lessons)) { console.warn(`[LEARNING_CONFIG] 챕터 ${chapterIndex}의 lessons가 유효하지 않습니다.`); return; } chapterData.lessons.forEach((lessonData, index) => { try { if (chapter.lessons[index] && lessonData && lessonData.completed !== undefined) { chapter.lessons[index].completed = lessonData.completed === true; } } catch (error) { console.error(`[LEARNING_CONFIG] 레슨 ${index} 업데이트 에러:`, error); } }); } } } catch (error) { if (typeof ErrorHandler !== 'undefined' && ErrorHandler) { ErrorHandler.handle(error, { context: 'LEARNING_CONFIG.loadFromHTML.chapter', chapterIndex }, false); } else { console.error(`[LEARNING_CONFIG] 챕터 ${chapterIndex} 업데이트 에러:`, error); } } }); console.log( "[LEARNING_CONFIG] learningConfigData 적용 완료:", LEARNING_CONFIG ); // 설정 유효성 검증 if (LEARNING_CONFIG.validate) { const isValid = LEARNING_CONFIG.validate(); if (!isValid) { console.warn('[LEARNING_CONFIG] 설정 유효성 검증 실패'); } } } } catch (error) { if (typeof ErrorHandler !== 'undefined' && ErrorHandler) { ErrorHandler.handle(error, { context: 'LEARNING_CONFIG.loadFromHTML' }, false); } else { console.error('[LEARNING_CONFIG] learningConfigData 적용 에러:', error); } } }