データベースを初期化中...
-しばらくお待ちください。
-
-
- 難易度:
-
-
-
-
-
- 問題形式:
-
-
-
+
+
+
+
+
+ 📈 学習進捗
+ +
+
+
-
-
+
-
-
-
+
+ コース名
+ 0% +
+
-
- SQLを直接入力して実行します
+
-
+ 0 / 0 レッスン完了
-
-
+
-
+
-
-
+
SQLクエリを入力してください
-
-
-
-
+
+
+
+
+
+
+ 📚 モジュール進捗
+
+
+
+
+
- 📍 現在の位置
+
+
+ モジュール名
+ >
+ レッスン名
+
+
+
+
+
-
-
+
+
+
-
-
-
+
📊 学習統計
+
+
+ 総スコア
+ 0
+
+
+ 学習日数
+ 0
+
+
+ 完了モジュール
+ 0
+
+
-
+
+
+
+
-
+
+
-
+
+
+
+
+
@@ -161,13 +260,13 @@
-
+
+ 解説スライド
+データベースを初期化中...
+しばらくお待ちください。
+
+
-
+ 難易度:
+
+
+
+
+
+ 問題形式:
+
+
+
+
+
+ SQLを直接入力して実行します
+
+
-
+
+
+
+
+
-
-
+
-
+
+ SQLクエリを入力してください
+
+
+
+
+
+
+
+
+
-
-
-
+
-
+
+ 解説スライド
+
+
+
+
-
- 実行結果
- -
-
- クエリを実行すると結果がここに表示されます
-
+
+
+
-
-
-
- 💡 ヒント
- -
+
+ 実行結果
+ +
+
+ クエリを実行すると結果がここに表示されます
+
+
+
+ 💡 ヒント
+ +💡 ヒント
AIが回答を分析中...
🎉
正解!
@@ -178,7 +277,7 @@
💡 ヒント
✨
@@ -201,6 +300,10 @@ 学習レポート - ${currentCourse.title}
+
+
+
+
+ • 発生した操作
+ • エラーメッセージ
+ • 使用しているブラウザ
+ • 現在の学習進捗 + `; + + this.show(supportMessage, 'info', { + duration: 0, + actions: [ + { + label: 'エラーログをコピー', + callback: () => this.copyErrorLog(), + primary: true + }, + { + label: '閉じる', + callback: () => {}, + primary: false + } + ] + }); + } + + /** + * エラーログをクリップボードにコピー + */ + async copyErrorLog() { + try { + if (window.errorHandler) { + const errorStats = window.errorHandler.getErrorStats(); + const logText = JSON.stringify(errorStats, null, 2); + + await navigator.clipboard.writeText(logText); + this.show('エラーログをクリップボードにコピーしました', 'success'); + } else { + this.show('エラーログが利用できません', 'warning'); + } + } catch (error) { + console.error('クリップボードへのコピーに失敗:', error); + this.show('クリップボードへのコピーに失敗しました', 'error'); + } + } + + /** + * システム状態の通知を表示 + * @param {Object} healthStatus - システム健全性情報 + */ + showSystemStatus(healthStatus) { + let message = ''; + let type = 'info'; + + switch (healthStatus.overall) { + case 'healthy': + message = 'システムは正常に動作しています'; + type = 'success'; + break; + case 'degraded': + message = 'システムの一部機能に制限があります'; + type = 'warning'; + break; + case 'unhealthy': + message = 'システムに問題が発生しています'; + type = 'error'; + break; + } + + const actions = []; + if (healthStatus.overall !== 'healthy') { + actions.push({ + label: '詳細を確認', + callback: () => console.log('システム状態:', healthStatus), + primary: true + }); + } + + this.show(message, type, { + duration: type === 'success' ? 3000 : 0, + actions: actions + }); + } +} + +// グローバルインスタンスを作成 +window.notificationSystem = new NotificationSystem(); + +export { NotificationSystem }; \ No newline at end of file diff --git a/js/progress-manager.js b/js/progress-manager.js new file mode 100644 index 0000000..9f2ff71 --- /dev/null +++ b/js/progress-manager.js @@ -0,0 +1,605 @@ +/** + * ProgressManager - 進捗管理システム + * localStorageを使用した進捗データの保存・読み込み、整合性チェック、エラー処理を提供 + */ +class ProgressManager { + constructor() { + this.storageKey = 'courseProgress'; + this.selectedCourseKey = 'selectedCourse'; + this.progressData = {}; + this.initialized = false; + } + + /** + * 進捗管理システムを初期化 + */ + async initialize() { + try { + this.loadProgressData(); + this.validateProgressData(); + this.initialized = true; + console.log('進捗管理システムが初期化されました'); + return true; + } catch (error) { + console.error('進捗管理システム初期化エラー:', error); + + // ErrorHandlerを使用して進捗データ破損を処理 + if (window.errorHandler) { + const result = await window.errorHandler.handleError('PROGRESS_DATA_CORRUPTION', error, { + operation: 'initialize', + storageKey: this.storageKey + }); + + if (result.success) { + // 復旧されたデータを使用 + if (result.recoveredData) { + this.progressData = result.recoveredData; + this.initialized = true; + return true; + } + } + } + + this.handleInitializationError(error); + return false; + } + } + + /** + * 進捗データをlocalStorageから読み込み + */ + loadProgressData() { + try { + const savedData = localStorage.getItem(this.storageKey); + if (savedData) { + const parsedData = JSON.parse(savedData); + this.progressData = this.migrateProgressData(parsedData); + } else { + this.progressData = {}; + } + } catch (error) { + console.error('進捗データ読み込みエラー:', error); + throw new Error('進捗データの読み込みに失敗しました'); + } + } + + /** + * 進捗データの形式を最新版にマイグレーション + */ + migrateProgressData(data) { + // 古い形式のデータを新しい形式に変換 + if (data.courseProgress) { + // 新しい形式の場合はそのまま返す + return data.courseProgress; + } else if (typeof data === 'object' && data !== null) { + // 直接コース進捗データが格納されている場合 + return data; + } + return {}; + } + + /** + * 進捗データの整合性をチェック + */ + validateProgressData() { + const validatedData = {}; + + for (const [courseId, progress] of Object.entries(this.progressData)) { + try { + const validatedProgress = this.validateCourseProgress(courseId, progress); + if (validatedProgress) { + validatedData[courseId] = validatedProgress; + } + } catch (error) { + console.warn(`コース ${courseId} の進捗データが無効です:`, error); + // 無効なデータは除外 + } + } + + this.progressData = validatedData; + } + + /** + * 個別コースの進捗データを検証 + */ + validateCourseProgress(courseId, progress) { + if (!progress || typeof progress !== 'object') { + throw new Error('進捗データが無効な形式です'); + } + + // 必須フィールドの存在チェック + const requiredFields = ['completedLessons', 'completedModules', 'startDate', 'lastAccessed']; + for (const field of requiredFields) { + if (!(field in progress)) { + console.warn(`必須フィールド ${field} が見つかりません。初期化します。`); + progress[field] = this.getDefaultFieldValue(field); + } + } + + // データ型の検証 + if (!Array.isArray(progress.completedLessons)) { + progress.completedLessons = []; + } + if (!Array.isArray(progress.completedModules)) { + progress.completedModules = []; + } + + // 日付の検証 + if (!this.isValidDate(progress.startDate)) { + progress.startDate = new Date().toISOString(); + } + if (!this.isValidDate(progress.lastAccessed)) { + progress.lastAccessed = new Date().toISOString(); + } + + // 数値の検証 + if (typeof progress.totalScore !== 'number' || progress.totalScore < 0) { + progress.totalScore = 0; + } + + // ブール値の検証 + if (typeof progress.isCompleted !== 'boolean') { + progress.isCompleted = false; + } + + return progress; + } + + /** + * デフォルトフィールド値を取得 + */ + getDefaultFieldValue(field) { + const defaults = { + completedLessons: [], + completedModules: [], + startDate: new Date().toISOString(), + lastAccessed: new Date().toISOString(), + currentModule: null, + currentLesson: null, + totalScore: 0, + isCompleted: false + }; + return defaults[field]; + } + + /** + * 日付文字列の有効性をチェック + */ + isValidDate(dateString) { + if (!dateString || typeof dateString !== 'string') { + return false; + } + const date = new Date(dateString); + return !isNaN(date.getTime()); + } + + /** + * コースの進捗データを取得 + */ + getCourseProgress(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + return this.progressData[courseId] || null; + } + + /** + * 全ての進捗データを取得 + */ + getAllProgress() { + return { ...this.progressData }; + } + + /** + * コースの進捗を初期化 + */ + initializeCourseProgress(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + const now = new Date().toISOString(); + this.progressData[courseId] = { + currentModule: null, + currentLesson: null, + completedLessons: [], + completedModules: [], + startDate: now, + lastAccessed: now, + totalScore: 0, + isCompleted: false + }; + + this.saveProgressData(); + console.log(`コース進捗を初期化しました: ${courseId}`); + return this.progressData[courseId]; + } + + /** + * レッスン完了を記録 + */ + markLessonCompleted(courseId, lessonId, score = 0) { + if (!courseId || !lessonId) { + throw new Error('コースIDまたはレッスンIDが指定されていません'); + } + + // 進捗データが存在しない場合は初期化 + if (!this.progressData[courseId]) { + this.initializeCourseProgress(courseId); + } + + const progress = this.progressData[courseId]; + + // レッスン完了をマーク(重複チェック) + if (!progress.completedLessons.includes(lessonId)) { + progress.completedLessons.push(lessonId); + } + + // 現在のレッスンを更新 + progress.currentLesson = lessonId; + progress.lastAccessed = new Date().toISOString(); + + // スコアを加算 + if (typeof score === 'number' && score > 0) { + progress.totalScore += score; + } + + this.saveProgressData(); + console.log(`レッスン完了を記録しました: ${courseId} - ${lessonId}`); + + return progress; + } + + /** + * モジュール完了を記録 + */ + markModuleCompleted(courseId, moduleId) { + if (!courseId || !moduleId) { + throw new Error('コースIDまたはモジュールIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + throw new Error(`コース ${courseId} の進捗データが見つかりません`); + } + + const progress = this.progressData[courseId]; + + // モジュール完了をマーク(重複チェック) + if (!progress.completedModules.includes(moduleId)) { + progress.completedModules.push(moduleId); + } + + progress.currentModule = moduleId; + progress.lastAccessed = new Date().toISOString(); + + this.saveProgressData(); + console.log(`モジュール完了を記録しました: ${courseId} - ${moduleId}`); + + return progress; + } + + /** + * コース完了を記録 + */ + markCourseCompleted(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + throw new Error(`コース ${courseId} の進捗データが見つかりません`); + } + + const progress = this.progressData[courseId]; + progress.isCompleted = true; + progress.lastAccessed = new Date().toISOString(); + + this.saveProgressData(); + console.log(`コース完了を記録しました: ${courseId}`); + + return progress; + } + + /** + * 進捗データをlocalStorageに保存 + */ + saveProgressData() { + try { + const dataToSave = { + courseProgress: this.progressData, + lastSaved: new Date().toISOString(), + version: '1.0' + }; + localStorage.setItem(this.storageKey, JSON.stringify(dataToSave)); + } catch (error) { + console.error('進捗データ保存エラー:', error); + this.handleSaveError(error); + } + } + + /** + * 選択されたコースを保存 + */ + saveSelectedCourse(courseId) { + try { + localStorage.setItem(this.selectedCourseKey, courseId); + } catch (error) { + console.error('選択コース保存エラー:', error); + } + } + + /** + * 選択されたコースを取得 + */ + getSelectedCourse() { + try { + return localStorage.getItem(this.selectedCourseKey); + } catch (error) { + console.error('選択コース取得エラー:', error); + return null; + } + } + + /** + * 特定コースの進捗をリセット + */ + resetCourseProgress(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + delete this.progressData[courseId]; + this.saveProgressData(); + console.log(`コース進捗をリセットしました: ${courseId}`); + } + + /** + * 全ての進捗をリセット + */ + resetAllProgress() { + this.progressData = {}; + try { + localStorage.removeItem(this.storageKey); + localStorage.removeItem(this.selectedCourseKey); + } catch (error) { + console.error('進捗リセットエラー:', error); + } + console.log('全ての進捗をリセットしました'); + } + + /** + * 進捗統計を取得 + */ + getProgressStats(courseId) { + const progress = this.getCourseProgress(courseId); + if (!progress) { + return null; + } + + return { + completedLessonsCount: progress.completedLessons.length, + completedModulesCount: progress.completedModules.length, + totalScore: progress.totalScore, + isCompleted: progress.isCompleted, + startDate: progress.startDate, + lastAccessed: progress.lastAccessed, + daysActive: this.calculateDaysActive(progress.startDate, progress.lastAccessed) + }; + } + + /** + * アクティブ日数を計算 + */ + calculateDaysActive(startDate, lastAccessed) { + try { + const start = new Date(startDate); + const last = new Date(lastAccessed); + const diffTime = Math.abs(last - start); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; + } catch (error) { + return 0; + } + } + + /** + * 初期化エラーの処理 + */ + handleInitializationError(error) { + console.error('進捗管理システムの初期化に失敗しました:', error); + + // 破損したデータをバックアップ + try { + const corruptedData = localStorage.getItem(this.storageKey); + if (corruptedData) { + localStorage.setItem(`${this.storageKey}_backup_${Date.now()}`, corruptedData); + } + } catch (backupError) { + console.error('バックアップ作成エラー:', backupError); + } + + // 進捗データをリセット + this.progressData = {}; + this.initialized = true; + } + + /** + * 保存エラーの処理 + */ + handleSaveError(error) { + if (error.name === 'QuotaExceededError') { + console.error('localStorageの容量が不足しています'); + // 古いバックアップデータを削除 + this.cleanupOldBackups(); + } else { + console.error('進捗データの保存に失敗しました:', error); + } + } + + /** + * 古いバックアップデータを削除 + */ + cleanupOldBackups() { + try { + const keys = Object.keys(localStorage); + const backupKeys = keys.filter(key => key.startsWith(`${this.storageKey}_backup_`)); + + // 古いバックアップから削除(最新5個を保持) + backupKeys.sort().slice(0, -5).forEach(key => { + localStorage.removeItem(key); + }); + } catch (error) { + console.error('バックアップクリーンアップエラー:', error); + } + } + + /** + * 進捗データの整合性をチェック(外部から呼び出し可能) + */ + validateDataIntegrity() { + const issues = []; + + for (const [courseId, progress] of Object.entries(this.progressData)) { + // 必須フィールドのチェック + const requiredFields = ['completedLessons', 'completedModules', 'startDate', 'lastAccessed']; + for (const field of requiredFields) { + if (!(field in progress)) { + issues.push(`${courseId}: 必須フィールド ${field} が見つかりません`); + } + } + + // データ型のチェック + if (!Array.isArray(progress.completedLessons)) { + issues.push(`${courseId}: completedLessons が配列ではありません`); + } + if (!Array.isArray(progress.completedModules)) { + issues.push(`${courseId}: completedModules が配列ではありません`); + } + + // 日付の妥当性チェック + if (!this.isValidDate(progress.startDate)) { + issues.push(`${courseId}: startDate が無効な日付です`); + } + if (!this.isValidDate(progress.lastAccessed)) { + issues.push(`${courseId}: lastAccessed が無効な日付です`); + } + } + + return { + isValid: issues.length === 0, + issues: issues + }; + } + + /** + * 初期化状態を確認 + */ + isInitialized() { + return this.initialized; + } + + /** + * デバッグ情報を取得 + */ + getDebugInfo() { + return { + initialized: this.initialized, + progressDataKeys: Object.keys(this.progressData), + selectedCourse: this.getSelectedCourse(), + storageUsage: this.getStorageUsage() + }; + } + + /** + * localStorage使用量を取得 + */ + getStorageUsage() { + try { + const data = localStorage.getItem(this.storageKey); + return data ? data.length : 0; + } catch (error) { + return -1; + } + } + + /** + * 最終アクセス日時を更新 + */ + updateLastAccessed(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + this.initializeCourseProgress(courseId); + } + + this.progressData[courseId].lastAccessed = new Date().toISOString(); + this.saveProgressData(); + } + + /** + * 完了済みレッスンを設定(修復用) + */ + setCompletedLessons(courseId, lessons) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + this.initializeCourseProgress(courseId); + } + + if (!Array.isArray(lessons)) { + throw new Error('レッスンリストは配列である必要があります'); + } + + this.progressData[courseId].completedLessons = [...lessons]; + this.progressData[courseId].lastAccessed = new Date().toISOString(); + this.saveProgressData(); + } + + /** + * 完了済みモジュールを設定(修復用) + */ + setCompletedModules(courseId, modules) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + this.initializeCourseProgress(courseId); + } + + if (!Array.isArray(modules)) { + throw new Error('モジュールリストは配列である必要があります'); + } + + this.progressData[courseId].completedModules = [...modules]; + this.progressData[courseId].lastAccessed = new Date().toISOString(); + this.saveProgressData(); + } + + /** + * 完了済みモジュールを削除(修復用) + */ + removeCompletedModule(courseId, moduleId) { + if (!courseId || !moduleId) { + throw new Error('コースIDまたはモジュールIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + return; + } + + const progress = this.progressData[courseId]; + const index = progress.completedModules.indexOf(moduleId); + if (index > -1) { + progress.completedModules.splice(index, 1); + progress.lastAccessed = new Date().toISOString(); + this.saveProgressData(); + } + } +} + +export { ProgressManager }; \ No newline at end of file diff --git a/js/progress-ui.js b/js/progress-ui.js new file mode 100644 index 0000000..98e69dc --- /dev/null +++ b/js/progress-ui.js @@ -0,0 +1,424 @@ +/** + * ProgressUI - 進捗表示UIの管理クラス + * 進捗インジケーター、モジュール構造表示、レッスン完了状態の可視化を担当 + */ +export class ProgressUI { + constructor(courseManager, gameEngine) { + this.courseManager = courseManager; + this.gameEngine = gameEngine; + this.isVisible = false; + this.currentCourse = null; + this.currentProgress = null; + + this.elements = { + progressPanel: document.getElementById('progress-panel'), + toggleButton: document.getElementById('toggle-progress-panel'), + showProgressBtn: document.getElementById('show-progress-btn'), + + // コース進捗要素 + currentCourseName: document.getElementById('current-course-name'), + courseProgressPercentage: document.getElementById('course-progress-percentage'), + courseProgressFill: document.getElementById('course-progress-fill'), + completedLessonsCount: document.getElementById('completed-lessons-count'), + totalLessonsCount: document.getElementById('total-lessons-count'), + + // モジュール進捗要素 + modulesList: document.getElementById('modules-list'), + + // 現在のレッスン要素 + currentModuleName: document.getElementById('current-module-name'), + currentLessonName: document.getElementById('current-lesson-name'), + prevLessonBtn: document.getElementById('prev-lesson-btn'), + nextLessonBtn: document.getElementById('next-lesson-btn'), + + // 統計要素 + totalScore: document.getElementById('total-score'), + learningDays: document.getElementById('learning-days'), + completedModulesCount: document.getElementById('completed-modules-count') + }; + + this.bindEvents(); + } + + /** + * イベントリスナーを設定 + */ + bindEvents() { + // パネル表示切り替え(ヘッダーボタン) + if (this.elements.showProgressBtn) { + this.elements.showProgressBtn.addEventListener('click', () => { + this.togglePanel(); + }); + } + + // パネル表示切り替え(パネル内ボタン) + if (this.elements.toggleButton) { + this.elements.toggleButton.addEventListener('click', () => { + this.togglePanel(); + }); + } + + // レッスンナビゲーション + if (this.elements.prevLessonBtn) { + this.elements.prevLessonBtn.addEventListener('click', () => { + this.navigateToPreviousLesson(); + }); + } + + if (this.elements.nextLessonBtn) { + this.elements.nextLessonBtn.addEventListener('click', () => { + this.navigateToNextLesson(); + }); + } + } + + /** + * 進捗パネルの表示/非表示を切り替え + */ + togglePanel() { + this.isVisible = !this.isVisible; + + if (this.isVisible) { + this.showPanel(); + } else { + this.hidePanel(); + } + } + + /** + * 進捗パネルを表示 + */ + showPanel() { + if (this.elements.progressPanel) { + this.elements.progressPanel.classList.remove('hidden'); + this.isVisible = true; + + // 現在のコースがある場合は進捗を更新 + if (this.currentCourse) { + this.updateProgressDisplay(); + } + } + } + + /** + * 進捗パネルを非表示 + */ + hidePanel() { + if (this.elements.progressPanel) { + this.elements.progressPanel.classList.add('hidden'); + this.isVisible = false; + } + } + + /** + * コースが選択された時の処理 + */ + onCourseSelected(course) { + this.currentCourse = course; + this.currentProgress = this.courseManager.getCourseProgress(course.id); + + if (this.isVisible) { + this.updateProgressDisplay(); + } + + // パネルを自動表示 + this.showPanel(); + } + + /** + * 進捗表示を更新 + */ + updateProgressDisplay() { + if (!this.currentCourse || !this.currentProgress) { + return; + } + + this.updateCourseProgress(); + this.updateModulesProgress(); + this.updateCurrentLessonInfo(); + this.updateLearningStats(); + } + + /** + * コース全体の進捗を更新 + */ + updateCourseProgress() { + const course = this.currentCourse; + const progress = this.currentProgress; + + // コース名を設定 + if (this.elements.currentCourseName) { + this.elements.currentCourseName.textContent = course.title; + } + + // 総レッスン数を計算 + const totalLessons = course.modules.reduce((total, module) => + total + module.lessons.length, 0 + ); + + const completedLessons = progress.completedLessons.length; + const progressPercentage = totalLessons > 0 ? + Math.round((completedLessons / totalLessons) * 100) : 0; + + // 進捗パーセンテージを更新 + if (this.elements.courseProgressPercentage) { + this.elements.courseProgressPercentage.textContent = `${progressPercentage}%`; + } + + // 進捗バーを更新 + if (this.elements.courseProgressFill) { + this.elements.courseProgressFill.style.width = `${progressPercentage}%`; + } + + // レッスン数を更新 + if (this.elements.completedLessonsCount) { + this.elements.completedLessonsCount.textContent = completedLessons; + } + if (this.elements.totalLessonsCount) { + this.elements.totalLessonsCount.textContent = totalLessons; + } + } + + /** + * モジュール進捗を更新 + */ + updateModulesProgress() { + const course = this.currentCourse; + const progress = this.currentProgress; + + if (!this.elements.modulesList) return; + + const modulesHtml = course.modules.map(module => { + const completedLessons = module.lessons.filter(lessonId => + progress.completedLessons.includes(lessonId) + ).length; + + const totalLessons = module.lessons.length; + const moduleProgress = totalLessons > 0 ? + Math.round((completedLessons / totalLessons) * 100) : 0; + + const isCompleted = progress.completedModules.includes(module.id); + const isCurrent = this.isCurrentModule(module.id); + + const moduleClasses = [ + 'module-item', + isCompleted ? 'completed' : '', + isCurrent ? 'current' : '' + ].filter(Boolean).join(' '); + + const statusIcon = isCompleted ? '✅' : + isCurrent ? '📍' : '⏳'; + + // レッスンドットを生成 + const lessonDots = module.lessons.map(lessonId => { + const isLessonCompleted = progress.completedLessons.includes(lessonId); + const isCurrentLesson = this.isCurrentLesson(lessonId); + + const dotClasses = [ + 'lesson-dot', + isLessonCompleted ? 'completed' : '', + isCurrentLesson ? 'current' : '' + ].filter(Boolean).join(' '); + + return ``; + }).join(''); + + return ` +
💡 ヒント
import * as duckdb from 'https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.28.0/+esm'; window.duckdb = duckdb; + + + + \ No newline at end of file diff --git a/js/adaptive-learning-ui.js b/js/adaptive-learning-ui.js new file mode 100644 index 0000000..0f4aec1 --- /dev/null +++ b/js/adaptive-learning-ui.js @@ -0,0 +1,620 @@ +/** + * AdaptiveLearningUI - 適応的学習機能のUI表示コンポーネント + * 学習推奨事項、困難な概念、追加練習問題の表示を管理 + */ +export class AdaptiveLearningUI { + constructor(adaptiveLearning, gameEngine, courseManager) { + this.adaptiveLearning = adaptiveLearning; + this.gameEngine = gameEngine; + this.courseManager = courseManager; + this.isVisible = false; + this.currentRecommendations = null; + this.init(); + } + + /** + * UI要素を初期化 + */ + init() { + this.createAdaptiveLearningPanel(); + this.attachEventListeners(); + } + + /** + * 適応的学習パネルを作成 + */ + createAdaptiveLearningPanel() { + // 既存のパネルがあれば削除 + const existingPanel = document.getElementById('adaptive-learning-panel'); + if (existingPanel) { + existingPanel.remove(); + } + + const panel = document.createElement('div'); + panel.id = 'adaptive-learning-panel'; + panel.className = 'adaptive-learning-panel'; + panel.innerHTML = ` +
+
+ 学習アシスタント
+ +
+
+ `;
+
+ // スタイルを追加
+ this.addStyles();
+
+ // パネルをページに追加
+ document.body.appendChild(panel);
+ }
+
+ /**
+ * スタイルを追加
+ */
+ addStyles() {
+ if (document.getElementById('adaptive-learning-styles')) {
+ return;
+ }
+
+ const style = document.createElement('style');
+ style.id = 'adaptive-learning-styles';
+ style.textContent = `
+ .adaptive-learning-panel {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 320px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ z-index: 1000;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ transition: transform 0.3s ease;
+ }
+
+ .adaptive-learning-panel.collapsed {
+ transform: translateX(280px);
+ }
+
+ .adaptive-learning-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border-radius: 8px 8px 0 0;
+ }
+
+ .adaptive-learning-header h3 {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
+ }
+
+ .adaptive-toggle {
+ background: rgba(255,255,255,0.2);
+ border: none;
+ border-radius: 4px;
+ color: white;
+ cursor: pointer;
+ padding: 4px 8px;
+ font-size: 16px;
+ transition: background 0.2s;
+ }
+
+ .adaptive-toggle:hover {
+ background: rgba(255,255,255,0.3);
+ }
+
+ .adaptive-learning-content {
+ padding: 16px;
+ max-height: 400px;
+ overflow-y: auto;
+ }
+
+ .recommendation-card {
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 6px;
+ padding: 12px;
+ margin-bottom: 12px;
+ }
+
+ .recommendation-card.high-priority {
+ border-left: 4px solid #dc3545;
+ }
+
+ .recommendation-card.medium-priority {
+ border-left: 4px solid #ffc107;
+ }
+
+ .recommendation-card.low-priority {
+ border-left: 4px solid #28a745;
+ }
+
+ .recommendation-title {
+ font-weight: 600;
+ font-size: 13px;
+ color: #495057;
+ margin-bottom: 4px;
+ }
+
+ .recommendation-description {
+ font-size: 12px;
+ color: #6c757d;
+ line-height: 1.4;
+ }
+
+ .concept-list {
+ list-style: none;
+ padding: 0;
+ margin: 8px 0 0 0;
+ }
+
+ .concept-item {
+ background: #fff3cd;
+ border: 1px solid #ffeaa7;
+ border-radius: 4px;
+ padding: 8px;
+ margin-bottom: 6px;
+ font-size: 12px;
+ }
+
+ .concept-success-rate {
+ font-weight: 600;
+ color: #856404;
+ }
+
+ .practice-button {
+ background: #007bff;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 6px 12px;
+ font-size: 11px;
+ cursor: pointer;
+ margin-top: 8px;
+ transition: background 0.2s;
+ }
+
+ .practice-button:hover {
+ background: #0056b3;
+ }
+
+ .loading {
+ text-align: center;
+ color: #6c757d;
+ font-size: 12px;
+ padding: 20px;
+ }
+
+ .no-data {
+ text-align: center;
+ color: #6c757d;
+ font-size: 12px;
+ padding: 20px;
+ }
+
+ .proficiency-indicator {
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: 12px;
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ }
+
+ .proficiency-expert {
+ background: #d4edda;
+ color: #155724;
+ }
+
+ .proficiency-proficient {
+ background: #d1ecf1;
+ color: #0c5460;
+ }
+
+ .proficiency-intermediate {
+ background: #fff3cd;
+ color: #856404;
+ }
+
+ .proficiency-beginner {
+ background: #f8d7da;
+ color: #721c24;
+ }
+
+ .proficiency-struggling {
+ background: #f5c6cb;
+ color: #721c24;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ /**
+ * イベントリスナーを設定
+ */
+ attachEventListeners() {
+ document.addEventListener('click', (e) => {
+ if (e.target.id === 'adaptive-toggle') {
+ this.togglePanel();
+ }
+
+ if (e.target.classList.contains('practice-button')) {
+ const conceptId = e.target.dataset.conceptId;
+ this.showAdditionalPractice(conceptId);
+ }
+ });
+
+ // ゲームエンジンのチャレンジ完了イベントをリッスン
+ if (this.gameEngine) {
+ const originalOnChallengeCompleted = this.gameEngine.onChallengeCompleted.bind(this.gameEngine);
+ this.gameEngine.onChallengeCompleted = () => {
+ originalOnChallengeCompleted();
+ // 少し遅延してから更新(データが保存されるのを待つ)
+ setTimeout(() => this.updateRecommendations(), 1000);
+ };
+ }
+ }
+
+ /**
+ * パネルの表示/非表示を切り替え
+ */
+ togglePanel() {
+ const panel = document.getElementById('adaptive-learning-panel');
+ if (panel) {
+ panel.classList.toggle('collapsed');
+ this.isVisible = !panel.classList.contains('collapsed');
+
+ if (this.isVisible) {
+ this.updateRecommendations();
+ }
+ }
+ }
+
+ /**
+ * 推奨事項を更新
+ */
+ async updateRecommendations() {
+ if (!this.adaptiveLearning || !this.adaptiveLearning.isInitialized()) {
+ this.showNoData('適応的学習システムが初期化されていません');
+ return;
+ }
+
+ const currentCourse = this.courseManager?.getCurrentCourse();
+ if (!currentCourse) {
+ this.showNoData('コースが選択されていません');
+ return;
+ }
+
+ try {
+ this.showLoading();
+
+ // 各種推奨事項を取得
+ const [
+ recommendations,
+ difficultConcepts,
+ proficiency
+ ] = await Promise.all([
+ this.getRecommendations(),
+ this.getDifficultConcepts(),
+ this.getUserProficiency()
+ ]);
+
+ this.displayRecommendations(recommendations, difficultConcepts, proficiency);
+ } catch (error) {
+ console.error('推奨事項更新エラー:', error);
+ this.showError('推奨事項の取得中にエラーが発生しました');
+ }
+ }
+
+ /**
+ * 推奨事項を取得
+ */
+ async getRecommendations() {
+ const currentChallenge = this.gameEngine?.getCurrentChallenge();
+ if (!currentChallenge || !currentChallenge.lessonId) {
+ return null;
+ }
+
+ const currentCourse = this.courseManager.getCurrentCourse();
+ return this.adaptiveLearning.recommendNextLesson(currentCourse.id, currentChallenge.lessonId);
+ }
+
+ /**
+ * 困難な概念を取得
+ */
+ async getDifficultConcepts() {
+ const currentCourse = this.courseManager.getCurrentCourse();
+ return this.adaptiveLearning.detectDifficultConcepts(currentCourse.id);
+ }
+
+ /**
+ * ユーザーの習熟度を取得
+ */
+ async getUserProficiency() {
+ const currentCourse = this.courseManager.getCurrentCourse();
+ return this.adaptiveLearning.analyzeUserProficiency(currentCourse.id);
+ }
+
+ /**
+ * 推奨事項を表示
+ */
+ displayRecommendations(recommendations, difficultConcepts, proficiency) {
+ const content = document.getElementById('adaptive-content');
+ if (!content) return;
+
+ let html = '';
+
+ // 次レッスンの推奨
+ if (recommendations) {
+ html += this.renderNextLessonRecommendation(recommendations);
+ }
+
+ // 困難な概念
+ if (difficultConcepts && difficultConcepts.length > 0) {
+ html += this.renderDifficultConcepts(difficultConcepts);
+ }
+
+ // 習熟度情報
+ if (proficiency) {
+ html += this.renderProficiencyInfo(proficiency);
+ }
+
+ if (!html) {
+ html = '学習データを分析中...
+ 学習データが不足しています。
いくつかの問題を解いてから再度確認してください。
';
+ }
+
+ content.innerHTML = html;
+ }
+
+ /**
+ * 次レッスン推奨を描画
+ */
+ renderNextLessonRecommendation(recommendations) {
+ const priorityClass = recommendations.confidence >= 0.8 ? 'high-priority' :
+ recommendations.confidence >= 0.6 ? 'medium-priority' : 'low-priority';
+
+ let html = `
+ いくつかの問題を解いてから再度確認してください。
+
';
+ return html;
+ }
+
+ /**
+ * 困難な概念を描画
+ */
+ renderDifficultConcepts(difficultConcepts) {
+ let html = `
+ 📚 次のステップ
+ ${recommendations.reason}
+ `;
+
+ if (recommendations.alternatives && recommendations.alternatives.length > 0) {
+ html += 'その他の選択肢:
';
+ recommendations.alternatives.forEach(alt => {
+ html += `• ${alt.description}
`;
+ });
+ }
+
+ html += '
+
';
+ return html;
+ }
+
+ /**
+ * 習熟度情報を描画
+ */
+ renderProficiencyInfo(proficiency) {
+ const overallPercent = Math.round(proficiency.overallProficiency * 100);
+ const velocityPercent = Math.round(proficiency.learningVelocity * 100);
+ const consistencyPercent = Math.round(proficiency.consistencyScore * 100);
+
+ return `
+ ⚠️ 重点学習が必要な概念
+ -
+ `;
+
+ difficultConcepts.slice(0, 3).forEach(concept => {
+ const successRate = Math.round(concept.successRate * 100);
+ html += `
+
-
+ ${concept.name}+正答率: ${successRate}%+ +
+ `;
+ });
+
+ html += '
+
+ `;
+ }
+
+ /**
+ * 追加練習問題を表示
+ */
+ showAdditionalPractice(conceptId) {
+ const currentCourse = this.courseManager.getCurrentCourse();
+ const practiceData = this.adaptiveLearning.suggestAdditionalPractice(currentCourse.id, conceptId);
+
+ if (!practiceData) {
+ alert('追加練習問題が見つかりませんでした。');
+ return;
+ }
+
+ // 簡単なモーダルで練習問題を表示
+ const modal = document.createElement('div');
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 2000;
+ `;
+
+ modal.innerHTML = `
+ 📊 学習状況
+
+
+ 全体習熟度: ${overallPercent}%
+ 学習速度: ${velocityPercent}%
+ 一貫性: ${consistencyPercent}%
+
+
+ `;
+
+ document.body.appendChild(modal);
+ }
+
+ /**
+ * ローディング表示
+ */
+ showLoading() {
+ const content = document.getElementById('adaptive-content');
+ if (content) {
+ content.innerHTML = '${practiceData.conceptName} - 追加練習
+現在の成功率: ${Math.round(practiceData.currentPerformance.successRate * 100)}%
+推定時間: ${practiceData.estimatedTime}分
+ +
+ ${practiceData.practiceProblems.map((problem, index) => `
+
+
+
+
+
+ `).join('')}
+ 練習問題 ${index + 1}
+${problem.question}
+
+
+ ヒント
+-
+ ${problem.hints.map(hint => `
- ${hint} `).join('')} +
学習データを分析中...
';
+ }
+ }
+
+ /**
+ * データなし表示
+ */
+ showNoData(message) {
+ const content = document.getElementById('adaptive-content');
+ if (content) {
+ content.innerHTML = `${message}
`;
+ }
+ }
+
+ /**
+ * エラー表示
+ */
+ showError(message) {
+ const content = document.getElementById('adaptive-content');
+ if (content) {
+ content.innerHTML = `${message}
`;
+ }
+ }
+
+ /**
+ * パネルを表示
+ */
+ show() {
+ const panel = document.getElementById('adaptive-learning-panel');
+ if (panel) {
+ panel.classList.remove('collapsed');
+ this.isVisible = true;
+ this.updateRecommendations();
+ }
+ }
+
+ /**
+ * パネルを非表示
+ */
+ hide() {
+ const panel = document.getElementById('adaptive-learning-panel');
+ if (panel) {
+ panel.classList.add('collapsed');
+ this.isVisible = false;
+ }
+ }
+
+ /**
+ * 学習レポートを表示
+ */
+ showLearningReport() {
+ const currentCourse = this.courseManager.getCurrentCourse();
+ const report = this.adaptiveLearning.generateLearningReport(currentCourse.id);
+
+ if (!report) {
+ alert('学習レポートを生成できませんでした。');
+ return;
+ }
+
+ // 新しいウィンドウで学習レポートを表示
+ const reportWindow = window.open('', '_blank', 'width=800,height=600');
+ reportWindow.document.write(`
+
+
+ 学習レポート
+
+
+
+ 概要
+完了レッスン: ${report.summary.completedLessons}
+学習時間: ${report.summary.totalTimeSpent}分
+全体習熟度: ${report.summary.overallProficiency}%
+学習速度: ${report.summary.learningVelocity}%
+一貫性: ${report.summary.consistencyScore}%
+
+
+
+ 強み
+ ${report.strengths.map(strength => + `• ${strength.conceptId} (${strength.successRate}%)
`
+ ).join('')}
+
+
+
+ 改善点
+ ${report.weaknesses.map(weakness => + `• ${weakness.name} (${Math.round(weakness.successRate * 100)}%)
`
+ ).join('')}
+
+
+
+
+ `);
+ }
+}
\ No newline at end of file
diff --git a/js/adaptive-learning.js b/js/adaptive-learning.js
new file mode 100644
index 0000000..f1eaec4
--- /dev/null
+++ b/js/adaptive-learning.js
@@ -0,0 +1,989 @@
+/**
+ * AdaptiveLearning - 適応的学習機能
+ * ユーザーの回答パフォーマンス追跡、困難な概念の検出、習熟度に応じた推奨機能を提供
+ */
+class AdaptiveLearning {
+ constructor(courseManager, gameEngine) {
+ this.courseManager = courseManager;
+ this.gameEngine = gameEngine;
+ this.performanceData = {};
+ this.difficultyThresholds = {
+ struggling: 0.4, // 40%以下の正答率で困難と判定
+ proficient: 0.8, // 80%以上の正答率で習熟と判定
+ minAttempts: 3 // 最低試行回数
+ };
+ this.conceptCategories = this.initializeConceptCategories();
+ this.initialized = false;
+ }
+
+ /**
+ * 適応的学習システムを初期化
+ */
+ async initialize() {
+ try {
+ this.loadPerformanceData();
+ this.initialized = true;
+ console.log('適応的学習システムが初期化されました');
+ return true;
+ } catch (error) {
+ console.error('適応的学習システム初期化エラー:', error);
+ this.performanceData = {};
+ this.initialized = true;
+ return false;
+ }
+ }
+
+ /**
+ * 概念カテゴリを初期化
+ */
+ initializeConceptCategories() {
+ return {
+ 'sql-basics': {
+ 'basic-select': {
+ name: 'SELECT文の基本',
+ keywords: ['SELECT', 'FROM', 'basic', 'table'],
+ difficulty: 1,
+ prerequisites: []
+ },
+ 'where-clause': {
+ name: 'WHERE句による絞り込み',
+ keywords: ['WHERE', 'filter', 'condition', '=', '>', '<'],
+ difficulty: 2,
+ prerequisites: ['basic-select']
+ },
+ 'order-limit': {
+ name: 'ORDER BYとLIMIT',
+ keywords: ['ORDER BY', 'LIMIT', 'ASC', 'DESC', 'sort'],
+ difficulty: 2,
+ prerequisites: ['basic-select']
+ },
+ 'aggregate': {
+ name: '集約関数',
+ keywords: ['COUNT', 'SUM', 'AVG', 'MAX', 'MIN', 'aggregate'],
+ difficulty: 3,
+ prerequisites: ['basic-select', 'where-clause']
+ },
+ 'group-by': {
+ name: 'GROUP BYとHAVING',
+ keywords: ['GROUP BY', 'HAVING', 'group', 'aggregate'],
+ difficulty: 4,
+ prerequisites: ['aggregate']
+ },
+ 'joins': {
+ name: 'テーブル結合',
+ keywords: ['JOIN', 'INNER', 'LEFT', 'RIGHT', 'ON', 'relationship'],
+ difficulty: 5,
+ prerequisites: ['basic-select', 'where-clause']
+ },
+ 'subqueries': {
+ name: 'サブクエリ',
+ keywords: ['subquery', 'EXISTS', 'IN', 'nested'],
+ difficulty: 6,
+ prerequisites: ['joins', 'aggregate']
+ }
+ },
+ 'db-fundamentals': {
+ 'ddl-basics': {
+ name: 'DDL基本操作',
+ keywords: ['CREATE', 'ALTER', 'DROP', 'TABLE', 'schema'],
+ difficulty: 3,
+ prerequisites: []
+ },
+ 'dml-operations': {
+ name: 'DML操作',
+ keywords: ['INSERT', 'UPDATE', 'DELETE', 'data manipulation'],
+ difficulty: 3,
+ prerequisites: ['ddl-basics']
+ },
+ 'constraints': {
+ name: '制約とインデックス',
+ keywords: ['PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'INDEX', 'constraint'],
+ difficulty: 4,
+ prerequisites: ['ddl-basics']
+ },
+ 'transactions': {
+ name: 'トランザクション管理',
+ keywords: ['TRANSACTION', 'COMMIT', 'ROLLBACK', 'ACID'],
+ difficulty: 5,
+ prerequisites: ['dml-operations']
+ }
+ },
+ 'big-data-basics': {
+ 'duckdb-features': {
+ name: 'DuckDBの特徴',
+ keywords: ['DuckDB', 'columnar', 'analytics', 'performance'],
+ difficulty: 4,
+ prerequisites: []
+ },
+ 'file-processing': {
+ name: 'ファイル処理',
+ keywords: ['CSV', 'Parquet', 'JSON', 'file', 'import'],
+ difficulty: 4,
+ prerequisites: ['duckdb-features']
+ },
+ 'window-functions': {
+ name: 'ウィンドウ関数',
+ keywords: ['WINDOW', 'ROW_NUMBER', 'RANK', 'LAG', 'LEAD'],
+ difficulty: 6,
+ prerequisites: ['file-processing']
+ },
+ 'performance-optimization': {
+ name: 'パフォーマンス最適化',
+ keywords: ['optimization', 'index', 'partition', 'parallel'],
+ difficulty: 7,
+ prerequisites: ['window-functions']
+ },
+ 'etl-pipelines': {
+ name: 'ETLパイプライン',
+ keywords: ['ETL', 'pipeline', 'workflow', 'automation'],
+ difficulty: 8,
+ prerequisites: ['performance-optimization']
+ }
+ }
+ };
+ }
+
+ /**
+ * ユーザーの回答パフォーマンスを追跡
+ */
+ trackPerformance(courseId, lessonId, challengeData) {
+ if (!courseId || !lessonId || !challengeData) {
+ console.warn('パフォーマンス追跡に必要なデータが不足しています');
+ return;
+ }
+
+ // コースのパフォーマンスデータを初期化
+ if (!this.performanceData[courseId]) {
+ this.performanceData[courseId] = {};
+ }
+
+ // レッスンのパフォーマンスデータを初期化
+ if (!this.performanceData[courseId][lessonId]) {
+ this.performanceData[courseId][lessonId] = {
+ attempts: [],
+ concepts: {},
+ firstAttemptTime: Date.now(),
+ lastAttemptTime: Date.now(),
+ totalTimeSpent: 0,
+ hintsUsed: 0,
+ averageScore: 0,
+ isCompleted: false
+ };
+ }
+
+ const lessonPerformance = this.performanceData[courseId][lessonId];
+
+ // 試行データを記録
+ const attemptData = {
+ timestamp: Date.now(),
+ correct: challengeData.correct || false,
+ score: challengeData.score || 0,
+ attempts: challengeData.attempts || 1,
+ hintsUsed: challengeData.hintsUsed || 0,
+ timeSpent: challengeData.timeSpent || 0,
+ errorType: challengeData.errorType || null,
+ concepts: this.identifyConcepts(courseId, challengeData.challenge)
+ };
+
+ lessonPerformance.attempts.push(attemptData);
+ lessonPerformance.lastAttemptTime = Date.now();
+ lessonPerformance.totalTimeSpent += attemptData.timeSpent;
+ lessonPerformance.hintsUsed += attemptData.hintsUsed;
+
+ // 概念別パフォーマンスを更新
+ this.updateConceptPerformance(lessonPerformance, attemptData);
+
+ // 平均スコアを計算
+ this.calculateAverageScore(lessonPerformance);
+
+ // 完了状態を更新
+ if (challengeData.correct) {
+ lessonPerformance.isCompleted = true;
+ }
+
+ // データを保存
+ this.savePerformanceData();
+
+ console.log(`パフォーマンス追跡: ${courseId} - ${lessonId}`, attemptData);
+ }
+
+ /**
+ * チャレンジから概念を識別
+ */
+ identifyConcepts(courseId, challenge) {
+ if (!challenge || !this.conceptCategories[courseId]) {
+ return [];
+ }
+
+ const identifiedConcepts = [];
+ const challengeText = (challenge.question + ' ' + (challenge.solution || '')).toLowerCase();
+
+ for (const [conceptId, concept] of Object.entries(this.conceptCategories[courseId])) {
+ const matchCount = concept.keywords.filter(keyword =>
+ challengeText.includes(keyword.toLowerCase())
+ ).length;
+
+ if (matchCount > 0) {
+ identifiedConcepts.push({
+ id: conceptId,
+ name: concept.name,
+ matchCount: matchCount,
+ difficulty: concept.difficulty
+ });
+ }
+ }
+
+ return identifiedConcepts.sort((a, b) => b.matchCount - a.matchCount);
+ }
+
+ /**
+ * 概念別パフォーマンスを更新
+ */
+ updateConceptPerformance(lessonPerformance, attemptData) {
+ attemptData.concepts.forEach(concept => {
+ if (!lessonPerformance.concepts[concept.id]) {
+ lessonPerformance.concepts[concept.id] = {
+ name: concept.name,
+ attempts: 0,
+ correctAttempts: 0,
+ totalScore: 0,
+ averageScore: 0,
+ successRate: 0,
+ difficulty: concept.difficulty,
+ strugglingIndicators: []
+ };
+ }
+
+ const conceptPerf = lessonPerformance.concepts[concept.id];
+ conceptPerf.attempts++;
+
+ if (attemptData.correct) {
+ conceptPerf.correctAttempts++;
+ }
+
+ conceptPerf.totalScore += attemptData.score;
+ conceptPerf.averageScore = conceptPerf.totalScore / conceptPerf.attempts;
+ conceptPerf.successRate = conceptPerf.correctAttempts / conceptPerf.attempts;
+
+ // 困難指標を記録
+ if (!attemptData.correct) {
+ conceptPerf.strugglingIndicators.push({
+ timestamp: attemptData.timestamp,
+ errorType: attemptData.errorType,
+ attempts: attemptData.attempts,
+ hintsUsed: attemptData.hintsUsed
+ });
+ }
+ });
+ }
+
+ /**
+ * 平均スコアを計算
+ */
+ calculateAverageScore(lessonPerformance) {
+ if (lessonPerformance.attempts.length === 0) {
+ lessonPerformance.averageScore = 0;
+ return;
+ }
+
+ const totalScore = lessonPerformance.attempts.reduce((sum, attempt) => sum + attempt.score, 0);
+ lessonPerformance.averageScore = totalScore / lessonPerformance.attempts.length;
+ }
+
+ /**
+ * 困難な概念を検出
+ */
+ detectDifficultConcepts(courseId, userId = 'default') {
+ if (!this.performanceData[courseId]) {
+ return [];
+ }
+
+ const difficultConcepts = [];
+ const conceptSummary = {};
+
+ // 全レッスンから概念パフォーマンスを集約
+ for (const [lessonId, lessonPerf] of Object.entries(this.performanceData[courseId])) {
+ for (const [conceptId, conceptPerf] of Object.entries(lessonPerf.concepts)) {
+ if (!conceptSummary[conceptId]) {
+ conceptSummary[conceptId] = {
+ name: conceptPerf.name,
+ totalAttempts: 0,
+ totalCorrect: 0,
+ totalScore: 0,
+ difficulty: conceptPerf.difficulty,
+ strugglingIndicators: [],
+ lessons: []
+ };
+ }
+
+ const summary = conceptSummary[conceptId];
+ summary.totalAttempts += conceptPerf.attempts;
+ summary.totalCorrect += conceptPerf.correctAttempts;
+ summary.totalScore += conceptPerf.totalScore;
+ summary.strugglingIndicators.push(...conceptPerf.strugglingIndicators);
+ summary.lessons.push(lessonId);
+ }
+ }
+
+ // 困難な概念を判定
+ for (const [conceptId, summary] of Object.entries(conceptSummary)) {
+ if (summary.totalAttempts >= this.difficultyThresholds.minAttempts) {
+ const successRate = summary.totalCorrect / summary.totalAttempts;
+ const averageScore = summary.totalScore / summary.totalAttempts;
+
+ if (successRate <= this.difficultyThresholds.struggling) {
+ difficultConcepts.push({
+ id: conceptId,
+ name: summary.name,
+ successRate: successRate,
+ averageScore: averageScore,
+ totalAttempts: summary.totalAttempts,
+ difficulty: summary.difficulty,
+ strugglingIndicators: summary.strugglingIndicators,
+ affectedLessons: summary.lessons,
+ recommendedActions: this.generateRecommendedActions(conceptId, summary)
+ });
+ }
+ }
+ }
+
+ return difficultConcepts.sort((a, b) => a.successRate - b.successRate);
+ }
+
+ /**
+ * 推奨アクションを生成
+ */
+ generateRecommendedActions(conceptId, summary) {
+ const actions = [];
+
+ // 基本的な復習の推奨
+ actions.push({
+ type: 'review',
+ title: '基本概念の復習',
+ description: `${summary.name}の基本的な使い方を復習しましょう`,
+ priority: 'high'
+ });
+
+ // 前提条件の確認
+ const courseId = Object.keys(this.performanceData)[0]; // 現在のコース
+ if (this.conceptCategories[courseId] && this.conceptCategories[courseId][conceptId]) {
+ const concept = this.conceptCategories[courseId][conceptId];
+ if (concept.prerequisites.length > 0) {
+ actions.push({
+ type: 'prerequisites',
+ title: '前提条件の確認',
+ description: `まず ${concept.prerequisites.join(', ')} を確実に理解しましょう`,
+ priority: 'high',
+ prerequisites: concept.prerequisites
+ });
+ }
+ }
+
+ // 追加練習問題の提案
+ if (summary.totalAttempts < 10) {
+ actions.push({
+ type: 'practice',
+ title: '追加練習問題',
+ description: `${summary.name}に関する追加の練習問題を解いてみましょう`,
+ priority: 'medium'
+ });
+ }
+
+ // エラーパターンに基づく具体的なアドバイス
+ const commonErrors = this.analyzeCommonErrors(summary.strugglingIndicators);
+ if (commonErrors.length > 0) {
+ actions.push({
+ type: 'error-analysis',
+ title: 'よくあるエラーの対策',
+ description: '特に注意すべきポイントを確認しましょう',
+ priority: 'medium',
+ commonErrors: commonErrors
+ });
+ }
+
+ return actions;
+ }
+
+ /**
+ * よくあるエラーを分析
+ */
+ analyzeCommonErrors(strugglingIndicators) {
+ const errorCounts = {};
+
+ strugglingIndicators.forEach(indicator => {
+ if (indicator.errorType) {
+ errorCounts[indicator.errorType] = (errorCounts[indicator.errorType] || 0) + 1;
+ }
+ });
+
+ return Object.entries(errorCounts)
+ .sort(([,a], [,b]) => b - a)
+ .slice(0, 3)
+ .map(([errorType, count]) => ({
+ type: errorType,
+ count: count,
+ advice: this.getErrorAdvice(errorType)
+ }));
+ }
+
+ /**
+ * エラータイプに応じたアドバイスを取得
+ */
+ getErrorAdvice(errorType) {
+ const adviceMap = {
+ 'syntax': 'SQL構文を再確認し、カンマやセミコロンの位置に注意しましょう',
+ 'column': '列名のスペルや存在を確認しましょう',
+ 'table': 'テーブル名が正しいか確認しましょう',
+ 'join': '結合条件を正しく指定しているか確認しましょう',
+ 'aggregate': '集約関数の使い方を復習しましょう',
+ 'logic': 'WHERE句の条件ロジックを見直しましょう'
+ };
+
+ return adviceMap[errorType] || '基本的な構文を再確認しましょう';
+ }
+
+ /**
+ * 追加練習問題を提案
+ */
+ suggestAdditionalPractice(courseId, conceptId) {
+ const difficultConcepts = this.detectDifficultConcepts(courseId);
+ const targetConcept = difficultConcepts.find(concept => concept.id === conceptId);
+
+ if (!targetConcept) {
+ return null;
+ }
+
+ // 概念の難易度に応じた練習問題を生成
+ const practiceProblems = this.generatePracticeProblems(courseId, conceptId, targetConcept);
+
+ return {
+ conceptId: conceptId,
+ conceptName: targetConcept.name,
+ currentPerformance: {
+ successRate: targetConcept.successRate,
+ averageScore: targetConcept.averageScore,
+ totalAttempts: targetConcept.totalAttempts
+ },
+ practiceProblems: practiceProblems,
+ estimatedTime: practiceProblems.length * 5, // 1問あたり5分と仮定
+ recommendedActions: targetConcept.recommendedActions
+ };
+ }
+
+ /**
+ * 練習問題を生成
+ */
+ generatePracticeProblems(courseId, conceptId, conceptData) {
+ const problems = [];
+
+ // 概念に基づいた基本的な練習問題テンプレート
+ const problemTemplates = {
+ 'basic-select': [
+ {
+ question: 'customers テーブルから全ての列を取得してください',
+ solution: 'SELECT * FROM customers;',
+ difficulty: 1
+ },
+ {
+ question: 'products テーブルから product_name と price 列のみを取得してください',
+ solution: 'SELECT product_name, price FROM products;',
+ difficulty: 1
+ }
+ ],
+ 'where-clause': [
+ {
+ question: 'customers テーブルから city が "Tokyo" の顧客を取得してください',
+ solution: 'SELECT * FROM customers WHERE city = "Tokyo";',
+ difficulty: 2
+ },
+ {
+ question: 'products テーブルから price が 1000 以上の商品を取得してください',
+ solution: 'SELECT * FROM products WHERE price >= 1000;',
+ difficulty: 2
+ }
+ ],
+ 'aggregate': [
+ {
+ question: 'orders テーブルの注文数を数えてください',
+ solution: 'SELECT COUNT(*) FROM orders;',
+ difficulty: 3
+ },
+ {
+ question: 'products テーブルの平均価格を計算してください',
+ solution: 'SELECT AVG(price) FROM products;',
+ difficulty: 3
+ }
+ ]
+ };
+
+ const templates = problemTemplates[conceptId] || [];
+
+ // 現在のパフォーマンスに基づいて問題を選択
+ const maxDifficulty = conceptData.successRate < 0.3 ? 2 :
+ conceptData.successRate < 0.6 ? 3 : 4;
+
+ templates.forEach((template, index) => {
+ if (template.difficulty <= maxDifficulty) {
+ problems.push({
+ id: `practice-${conceptId}-${index}`,
+ question: template.question,
+ solution: template.solution,
+ difficulty: template.difficulty,
+ type: 'practice',
+ conceptId: conceptId,
+ hints: this.generateHints(template)
+ });
+ }
+ });
+
+ return problems;
+ }
+
+ /**
+ * ヒントを生成
+ */
+ generateHints(problemTemplate) {
+ const hints = [];
+
+ if (problemTemplate.solution.includes('SELECT')) {
+ hints.push('SELECT文を使用してデータを取得します');
+ }
+ if (problemTemplate.solution.includes('WHERE')) {
+ hints.push('WHERE句を使用して条件を指定します');
+ }
+ if (problemTemplate.solution.includes('COUNT') || problemTemplate.solution.includes('AVG')) {
+ hints.push('集約関数を使用して計算を行います');
+ }
+
+ hints.push(`正解例: ${problemTemplate.solution}`);
+
+ return hints;
+ }
+
+ /**
+ * 習熟度に応じた次レッスンの推奨
+ */
+ recommendNextLesson(courseId, currentLessonId) {
+ if (!this.courseManager || !this.courseManager.getCurrentCourse()) {
+ return null;
+ }
+
+ const course = this.courseManager.getCurrentCourse();
+ const progress = this.courseManager.getCurrentCourseProgress();
+ const userPerformance = this.analyzeUserProficiency(courseId);
+
+ // 現在のレッスンのパフォーマンスを確認
+ const currentPerformance = this.performanceData[courseId]?.[currentLessonId];
+
+ let recommendation = {
+ type: 'next',
+ lessonId: null,
+ reason: '',
+ confidence: 0,
+ alternatives: []
+ };
+
+ // 習熟度に基づく判定
+ if (currentPerformance) {
+ const successRate = currentPerformance.concepts ?
+ this.calculateOverallSuccessRate(currentPerformance.concepts) : 0;
+
+ if (successRate >= this.difficultyThresholds.proficient) {
+ // 高い習熟度 - 次のレッスンまたは高度なチャレンジを推奨
+ recommendation = this.recommendAdvancedPath(course, progress, userPerformance);
+ } else if (successRate <= this.difficultyThresholds.struggling) {
+ // 低い習熟度 - 復習または基礎固めを推奨
+ recommendation = this.recommendReviewPath(courseId, currentLessonId, userPerformance);
+ } else {
+ // 標準的な習熟度 - 通常の進行を推奨
+ recommendation = this.recommendStandardPath(course, progress);
+ }
+ } else {
+ // パフォーマンスデータがない場合は標準進行
+ recommendation = this.recommendStandardPath(course, progress);
+ }
+
+ return recommendation;
+ }
+
+ /**
+ * 全体的な成功率を計算
+ */
+ calculateOverallSuccessRate(concepts) {
+ if (!concepts || Object.keys(concepts).length === 0) {
+ return 0;
+ }
+
+ const rates = Object.values(concepts).map(concept => concept.successRate);
+ return rates.reduce((sum, rate) => sum + rate, 0) / rates.length;
+ }
+
+ /**
+ * 高度なパスを推奨
+ */
+ recommendAdvancedPath(course, progress, userPerformance) {
+ // 次の利用可能なレッスンを取得
+ const nextLesson = this.courseManager.getNextLesson(course.id);
+
+ if (nextLesson) {
+ return {
+ type: 'advanced',
+ lessonId: nextLesson.lessonId,
+ reason: '高い習熟度を示しているため、次のレッスンに進むことをお勧めします',
+ confidence: 0.9,
+ alternatives: [
+ {
+ type: 'challenge',
+ description: 'より高度なチャレンジ問題に挑戦',
+ confidence: 0.7
+ }
+ ]
+ };
+ }
+
+ return {
+ type: 'completion',
+ lessonId: null,
+ reason: 'コースを完了しました!次のコースに進むことをお勧めします',
+ confidence: 1.0,
+ alternatives: []
+ };
+ }
+
+ /**
+ * 復習パスを推奨
+ */
+ recommendReviewPath(courseId, currentLessonId, userPerformance) {
+ const difficultConcepts = this.detectDifficultConcepts(courseId);
+
+ if (difficultConcepts.length > 0) {
+ const mostDifficult = difficultConcepts[0];
+
+ return {
+ type: 'review',
+ lessonId: currentLessonId, // 現在のレッスンを復習
+ reason: `${mostDifficult.name}の理解を深めるため、復習をお勧めします`,
+ confidence: 0.8,
+ alternatives: [
+ {
+ type: 'practice',
+ description: '追加の練習問題で基礎を固める',
+ confidence: 0.9,
+ conceptId: mostDifficult.id
+ },
+ {
+ type: 'prerequisites',
+ description: '前提条件となる概念を復習する',
+ confidence: 0.7
+ }
+ ],
+ difficultConcepts: difficultConcepts.slice(0, 3)
+ };
+ }
+
+ return {
+ type: 'retry',
+ lessonId: currentLessonId,
+ reason: '理解を深めるため、もう一度挑戦してみましょう',
+ confidence: 0.6,
+ alternatives: []
+ };
+ }
+
+ /**
+ * 標準パスを推奨
+ */
+ recommendStandardPath(course, progress) {
+ const nextLesson = this.courseManager.getNextLesson(course.id);
+
+ if (nextLesson) {
+ return {
+ type: 'standard',
+ lessonId: nextLesson.lessonId,
+ reason: '順調に進んでいます。次のレッスンに進みましょう',
+ confidence: 0.8,
+ alternatives: []
+ };
+ }
+
+ return {
+ type: 'completion',
+ lessonId: null,
+ reason: 'コースを完了しました!',
+ confidence: 1.0,
+ alternatives: []
+ };
+ }
+
+ /**
+ * ユーザーの習熟度を分析
+ */
+ analyzeUserProficiency(courseId) {
+ if (!this.performanceData[courseId]) {
+ return {
+ overallProficiency: 0,
+ conceptProficiency: {},
+ learningVelocity: 0,
+ consistencyScore: 0
+ };
+ }
+
+ const courseData = this.performanceData[courseId];
+ const allConcepts = {};
+ let totalAttempts = 0;
+ let totalCorrect = 0;
+ let totalTime = 0;
+
+ // 全レッスンのデータを集約
+ for (const lessonData of Object.values(courseData)) {
+ totalAttempts += lessonData.attempts.length;
+ totalCorrect += lessonData.attempts.filter(a => a.correct).length;
+ totalTime += lessonData.totalTimeSpent;
+
+ // 概念別データを集約
+ for (const [conceptId, conceptData] of Object.entries(lessonData.concepts)) {
+ if (!allConcepts[conceptId]) {
+ allConcepts[conceptId] = {
+ attempts: 0,
+ correct: 0,
+ totalScore: 0
+ };
+ }
+ allConcepts[conceptId].attempts += conceptData.attempts;
+ allConcepts[conceptId].correct += conceptData.correctAttempts;
+ allConcepts[conceptId].totalScore += conceptData.totalScore;
+ }
+ }
+
+ // 習熟度指標を計算
+ const overallProficiency = totalAttempts > 0 ? totalCorrect / totalAttempts : 0;
+
+ const conceptProficiency = {};
+ for (const [conceptId, data] of Object.entries(allConcepts)) {
+ conceptProficiency[conceptId] = {
+ successRate: data.attempts > 0 ? data.correct / data.attempts : 0,
+ averageScore: data.attempts > 0 ? data.totalScore / data.attempts : 0,
+ proficiencyLevel: this.calculateProficiencyLevel(data.correct / data.attempts)
+ };
+ }
+
+ return {
+ overallProficiency: overallProficiency,
+ conceptProficiency: conceptProficiency,
+ learningVelocity: this.calculateLearningVelocity(courseData),
+ consistencyScore: this.calculateConsistencyScore(courseData)
+ };
+ }
+
+ /**
+ * 習熟度レベルを計算
+ */
+ calculateProficiencyLevel(successRate) {
+ if (successRate >= 0.9) return 'expert';
+ if (successRate >= 0.8) return 'proficient';
+ if (successRate >= 0.6) return 'intermediate';
+ if (successRate >= 0.4) return 'beginner';
+ return 'struggling';
+ }
+
+ /**
+ * 学習速度を計算
+ */
+ calculateLearningVelocity(courseData) {
+ const lessonTimes = Object.values(courseData)
+ .filter(lesson => lesson.isCompleted)
+ .map(lesson => lesson.totalTimeSpent);
+
+ if (lessonTimes.length < 2) return 0;
+
+ // 最近のレッスンほど重みを大きくして平均時間を計算
+ const weightedSum = lessonTimes.reduce((sum, time, index) => {
+ const weight = (index + 1) / lessonTimes.length;
+ return sum + (time * weight);
+ }, 0);
+
+ const weightSum = lessonTimes.reduce((sum, _, index) => {
+ return sum + ((index + 1) / lessonTimes.length);
+ }, 0);
+
+ const averageTime = weightedSum / weightSum;
+
+ // 速度スコア(短時間ほど高スコア)
+ return Math.max(0, 1 - (averageTime / (30 * 60 * 1000))); // 30分を基準
+ }
+
+ /**
+ * 一貫性スコアを計算
+ */
+ calculateConsistencyScore(courseData) {
+ const successRates = Object.values(courseData)
+ .filter(lesson => lesson.attempts.length >= 3)
+ .map(lesson => {
+ const correct = lesson.attempts.filter(a => a.correct).length;
+ return correct / lesson.attempts.length;
+ });
+
+ if (successRates.length < 2) return 0;
+
+ // 標準偏差を計算(低いほど一貫性が高い)
+ const mean = successRates.reduce((sum, rate) => sum + rate, 0) / successRates.length;
+ const variance = successRates.reduce((sum, rate) => sum + Math.pow(rate - mean, 2), 0) / successRates.length;
+ const standardDeviation = Math.sqrt(variance);
+
+ // 一貫性スコア(標準偏差が小さいほど高スコア)
+ return Math.max(0, 1 - (standardDeviation * 2));
+ }
+
+ /**
+ * パフォーマンスデータを保存
+ */
+ savePerformanceData() {
+ try {
+ const dataToSave = {
+ performanceData: this.performanceData,
+ lastUpdated: new Date().toISOString(),
+ version: '1.0'
+ };
+ localStorage.setItem('adaptiveLearningData', JSON.stringify(dataToSave));
+ } catch (error) {
+ console.error('適応的学習データ保存エラー:', error);
+ }
+ }
+
+ /**
+ * パフォーマンスデータを読み込み
+ */
+ loadPerformanceData() {
+ try {
+ const savedData = localStorage.getItem('adaptiveLearningData');
+ if (savedData) {
+ const parsedData = JSON.parse(savedData);
+ this.performanceData = parsedData.performanceData || {};
+ } else {
+ this.performanceData = {};
+ }
+ } catch (error) {
+ console.error('適応的学習データ読み込みエラー:', error);
+ this.performanceData = {};
+ }
+ }
+
+ /**
+ * 学習レポートを生成
+ */
+ generateLearningReport(courseId) {
+ const proficiency = this.analyzeUserProficiency(courseId);
+ const difficultConcepts = this.detectDifficultConcepts(courseId);
+ const courseData = this.performanceData[courseId] || {};
+
+ const completedLessons = Object.keys(courseData).filter(
+ lessonId => courseData[lessonId].isCompleted
+ ).length;
+
+ const totalTime = Object.values(courseData).reduce(
+ (sum, lesson) => sum + lesson.totalTimeSpent, 0
+ );
+
+ return {
+ courseId: courseId,
+ generatedAt: new Date().toISOString(),
+ summary: {
+ completedLessons: completedLessons,
+ totalTimeSpent: Math.round(totalTime / (1000 * 60)), // 分単位
+ overallProficiency: Math.round(proficiency.overallProficiency * 100),
+ learningVelocity: Math.round(proficiency.learningVelocity * 100),
+ consistencyScore: Math.round(proficiency.consistencyScore * 100)
+ },
+ strengths: this.identifyStrengths(proficiency.conceptProficiency),
+ weaknesses: difficultConcepts.slice(0, 5),
+ recommendations: this.generateOverallRecommendations(proficiency, difficultConcepts),
+ nextSteps: this.recommendNextLesson(courseId, null)
+ };
+ }
+
+ /**
+ * 強みを特定
+ */
+ identifyStrengths(conceptProficiency) {
+ return Object.entries(conceptProficiency)
+ .filter(([_, data]) => data.proficiencyLevel === 'expert' || data.proficiencyLevel === 'proficient')
+ .sort(([_, a], [__, b]) => b.successRate - a.successRate)
+ .slice(0, 5)
+ .map(([conceptId, data]) => ({
+ conceptId: conceptId,
+ successRate: Math.round(data.successRate * 100),
+ averageScore: Math.round(data.averageScore),
+ level: data.proficiencyLevel
+ }));
+ }
+
+ /**
+ * 全体的な推奨事項を生成
+ */
+ generateOverallRecommendations(proficiency, difficultConcepts) {
+ const recommendations = [];
+
+ // 習熟度に基づく推奨
+ if (proficiency.overallProficiency >= 0.8) {
+ recommendations.push({
+ type: 'advancement',
+ title: '次のレベルへ',
+ description: '高い習熟度を示しています。より高度なトピックに挑戦しましょう。'
+ });
+ } else if (proficiency.overallProficiency <= 0.4) {
+ recommendations.push({
+ type: 'foundation',
+ title: '基礎固め',
+ description: '基本概念の理解を深めることに集中しましょう。'
+ });
+ }
+
+ // 困難な概念に基づく推奨
+ if (difficultConcepts.length > 0) {
+ recommendations.push({
+ type: 'focus',
+ title: '重点学習',
+ description: `特に ${difficultConcepts[0].name} の理解を深めることをお勧めします。`
+ });
+ }
+
+ // 学習速度に基づく推奨
+ if (proficiency.learningVelocity < 0.3) {
+ recommendations.push({
+ type: 'pace',
+ title: '学習ペース',
+ description: 'じっくりと時間をかけて理解を深めることが大切です。'
+ });
+ } else if (proficiency.learningVelocity > 0.8) {
+ recommendations.push({
+ type: 'challenge',
+ title: 'チャレンジ',
+ description: '学習速度が速いので、より難しい問題に挑戦してみましょう。'
+ });
+ }
+
+ return recommendations;
+ }
+
+ /**
+ * 初期化状態を確認
+ */
+ isInitialized() {
+ return this.initialized;
+ }
+
+ /**
+ * デバッグ情報を取得
+ */
+ getDebugInfo() {
+ return {
+ initialized: this.initialized,
+ performanceDataKeys: Object.keys(this.performanceData),
+ conceptCategories: Object.keys(this.conceptCategories),
+ thresholds: this.difficultyThresholds
+ };
+ }
+}
+
+// Export for ES6 modules
+export { AdaptiveLearning };
\ No newline at end of file
diff --git a/js/course-manager.js b/js/course-manager.js
new file mode 100644
index 0000000..d35656a
--- /dev/null
+++ b/js/course-manager.js
@@ -0,0 +1,1494 @@
+/**
+ * CourseManager - コースシステムの管理クラス
+ * コースの読み込み、進捗管理、レッスンアンロック機能を提供
+ */
+class CourseManager {
+ constructor() {
+ this.courses = [];
+ this.currentCourse = null;
+ this.challengeData = {};
+ this.initialized = false;
+ this.progressManager = null;
+ }
+
+ /**
+ * ProgressManagerを設定
+ */
+ setProgressManager(progressManager) {
+ this.progressManager = progressManager;
+ }
+
+ /**
+ * コース定義を読み込み
+ */
+ async loadCourses() {
+ try {
+ if (!this.progressManager) {
+ throw new Error('ProgressManagerが設定されていません。setProgressManager()を先に呼び出してください。');
+ }
+
+ const response = await fetch('data/courses.json');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ this.courses = data.courses;
+
+ // 各コースのチャレンジデータを読み込み
+ await this.loadChallengeData();
+
+ // 進捗管理システムを初期化
+ this.progressManager.initialize();
+
+ // 選択されたコースを復元
+ this.restoreSelectedCourse();
+
+ this.initialized = true;
+ console.log('コースデータの読み込みが完了しました');
+ return this.courses;
+ } catch (error) {
+ console.error('コース読み込みエラー:', error);
+
+ // ErrorHandlerを使用してエラーを処理
+ if (window.errorHandler) {
+ const result = await window.errorHandler.handleError('COURSE_LOAD_ERROR', error, {
+ operation: 'loadCourses',
+ url: 'data/courses.json'
+ });
+
+ if (result.success) {
+ this.courses = result.data;
+ this.initialized = true;
+ return this.courses;
+ }
+ }
+
+ // フォールバック: デフォルトコースを読み込み
+ return this.loadDefaultCourse();
+ }
+ }
+
+ /**
+ * 各コースのチャレンジデータを読み込み
+ */
+ async loadChallengeData() {
+ const challengeFiles = {
+ 'sql-basics': 'slides/challenges.json',
+ 'db-fundamentals': 'slides/db-fundamentals-challenges.json',
+ 'big-data-basics': 'slides/big-data-basics-challenges.json'
+ };
+
+ for (const [courseId, filePath] of Object.entries(challengeFiles)) {
+ try {
+ const response = await fetch(filePath);
+ if (response.ok) {
+ this.challengeData[courseId] = await response.json();
+ } else {
+ console.warn(`チャレンジファイルが見つかりません: ${filePath}`);
+ this.challengeData[courseId] = [];
+ }
+ } catch (error) {
+ console.error(`チャレンジデータ読み込みエラー (${courseId}):`, error);
+
+ // ErrorHandlerを使用してチャレンジデータエラーを処理
+ if (window.errorHandler) {
+ const result = await window.errorHandler.handleError('CHALLENGE_LOAD_ERROR', error, {
+ courseId: courseId,
+ challengeFile: filePath,
+ operation: 'loadChallengeData'
+ });
+
+ if (result.success) {
+ this.challengeData[courseId] = result.data;
+ } else {
+ this.challengeData[courseId] = [];
+ }
+ } else {
+ this.challengeData[courseId] = [];
+ }
+ }
+ }
+ }
+
+ /**
+ * デフォルトコースを読み込み(エラー時のフォールバック)
+ */
+ async loadDefaultCourse() {
+ console.log('デフォルトコースを読み込み中...');
+ this.courses = [{
+ id: 'sql-basics',
+ title: 'SQL基礎コース',
+ description: 'デフォルトのSQL基礎コース',
+ targetAudience: '初心者',
+ difficulty: '初級',
+ estimatedHours: 8,
+ modules: [{
+ id: 'module-1',
+ title: '基本操作',
+ description: 'SQL基本操作',
+ lessons: ['challenge-001', 'challenge-002'],
+ prerequisites: []
+ }]
+ }];
+
+ // 既存のchallenges.jsonを読み込み
+ try {
+ const response = await fetch('slides/challenges.json');
+ if (response.ok) {
+ this.challengeData['sql-basics'] = await response.json();
+ }
+ } catch (error) {
+ console.error('デフォルトチャレンジ読み込みエラー:', error);
+ }
+
+ this.initialized = true;
+ return this.courses;
+ }
+
+ /**
+ * 利用可能なコース一覧を取得
+ */
+ getCourses() {
+ return this.courses;
+ }
+
+ /**
+ * 特定のコースを取得
+ */
+ getCourse(courseId) {
+ return this.courses.find(course => course.id === courseId);
+ }
+
+ /**
+ * 現在選択中のコースを取得
+ */
+ getCurrentCourse() {
+ return this.currentCourse;
+ }
+
+ /**
+ * コースを選択
+ */
+ selectCourse(courseId) {
+ const course = this.getCourse(courseId);
+ if (!course) {
+ throw new Error(`コースが見つかりません: ${courseId}`);
+ }
+
+ this.currentCourse = course;
+ this.progressManager.saveSelectedCourse(courseId);
+
+ // 進捗データが存在しない場合は初期化
+ if (!this.progressManager.getCourseProgress(courseId)) {
+ this.progressManager.initializeCourseProgress(courseId);
+ }
+
+ console.log(`コースを選択しました: ${course.title}`);
+ return course;
+ }
+
+ /**
+ * コースの進捗を取得
+ */
+ getCourseProgress(courseId) {
+ return this.progressManager.getCourseProgress(courseId);
+ }
+
+ /**
+ * 現在のコースの進捗を取得
+ */
+ getCurrentCourseProgress() {
+ if (!this.currentCourse) return null;
+ return this.progressManager.getCourseProgress(this.currentCourse.id);
+ }
+
+ /**
+ * 進捗を更新
+ */
+ updateProgress(courseId, lessonId, score = 0) {
+ // ProgressManagerを使用してレッスン完了を記録
+ this.progressManager.markLessonCompleted(courseId, lessonId, score);
+
+ // レッスン完了時の自動アンロック処理
+ const unlockedLessons = this.processLessonCompletion(courseId, lessonId);
+
+ // モジュール完了チェック
+ const completedModules = this.checkModuleCompletion(courseId);
+
+ // モジュール完了時の処理(概念要約と自動アンロック)
+ let moduleCompletionResults = [];
+ if (completedModules.length > 0) {
+ moduleCompletionResults = this.processModuleCompletionWithSummary(courseId, completedModules);
+ console.log(`モジュール完了処理が実行されました:`, moduleCompletionResults);
+ }
+
+ // コース完了チェック
+ this.checkCourseCompletion(courseId);
+
+ console.log(`進捗を更新しました: ${courseId} - ${lessonId}`);
+
+ // 拡張されたアンロック情報を返す
+ return {
+ lessonId: lessonId,
+ score: score,
+ unlockedLessons: unlockedLessons,
+ completedModules: completedModules,
+ moduleCompletionResults: moduleCompletionResults
+ };
+ }
+
+ /**
+ * レッスン完了時の自動アンロック処理
+ */
+ processLessonCompletion(courseId, completedLessonId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return [];
+
+ const newlyUnlockedLessons = [];
+
+ // 完了したレッスンが属するモジュールを見つける
+ let completedLessonModule = null;
+ for (const module of course.modules) {
+ if (module.lessons.includes(completedLessonId)) {
+ completedLessonModule = module;
+ break;
+ }
+ }
+
+ if (!completedLessonModule) return [];
+
+ // 同じモジュール内の次のレッスンをチェック
+ const lessonIndex = completedLessonModule.lessons.indexOf(completedLessonId);
+ if (lessonIndex >= 0 && lessonIndex < completedLessonModule.lessons.length - 1) {
+ const nextLessonId = completedLessonModule.lessons[lessonIndex + 1];
+
+ // 次のレッスンがまだ完了していない場合、アンロック状態をチェック
+ if (!progress.completedLessons.includes(nextLessonId)) {
+ if (this.isLessonUnlocked(courseId, nextLessonId)) {
+ newlyUnlockedLessons.push({
+ lessonId: nextLessonId,
+ moduleId: completedLessonModule.id,
+ moduleTitle: completedLessonModule.title,
+ reason: 'previous_lesson_completed'
+ });
+ console.log(`レッスン完了により次のレッスンがアンロックされました: ${nextLessonId}`);
+ }
+ }
+ }
+
+ return newlyUnlockedLessons;
+ }
+
+ /**
+ * モジュール完了時の自動アンロック処理
+ */
+ processModuleCompletion(courseId, completedModuleIds) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress || !completedModuleIds.length) return [];
+
+ const newlyUnlockedModules = [];
+
+ // 各モジュールをチェックして、新たにアンロックされるモジュールを見つける
+ for (const module of course.modules) {
+ // まだ開始されていないモジュールのみチェック
+ if (!progress.completedModules.includes(module.id)) {
+ // 前提条件がすべて満たされているかチェック
+ const allPrerequisitesMet = module.prerequisites.every(prereqId =>
+ progress.completedModules.includes(prereqId)
+ );
+
+ if (allPrerequisitesMet && this.isModuleUnlocked(courseId, module.id)) {
+ // モジュールの最初のレッスンがアンロックされているかチェック
+ if (module.lessons.length > 0) {
+ const firstLessonId = module.lessons[0];
+ if (this.isLessonUnlocked(courseId, firstLessonId)) {
+ newlyUnlockedModules.push({
+ moduleId: module.id,
+ moduleTitle: module.title,
+ firstLessonId: firstLessonId,
+ reason: 'prerequisites_completed',
+ completedPrerequisites: completedModuleIds.filter(id =>
+ module.prerequisites.includes(id)
+ )
+ });
+ console.log(`モジュール完了により新しいモジュールがアンロックされました: ${module.title}`);
+ }
+ }
+ }
+ }
+ }
+
+ return newlyUnlockedModules;
+ }
+
+ /**
+ * 次のレッスンを取得
+ */
+ getNextLesson(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return null;
+
+ // 全モジュールから全レッスンを取得
+ const allLessons = [];
+ course.modules.forEach(module => {
+ module.lessons.forEach(lessonId => {
+ allLessons.push({
+ lessonId,
+ moduleId: module.id,
+ moduleTitle: module.title
+ });
+ });
+ });
+
+ // 完了していない最初のレッスンを探す
+ for (const lesson of allLessons) {
+ if (!progress.completedLessons.includes(lesson.lessonId)) {
+ // レッスンがアンロックされているかチェック
+ if (this.isLessonUnlocked(courseId, lesson.lessonId)) {
+ return lesson;
+ }
+ }
+ }
+
+ return null; // 全レッスン完了
+ }
+
+ /**
+ * レッスンがアンロックされているかチェック
+ */
+ isLessonUnlocked(courseId, lessonId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) {
+ console.warn(`コースまたは進捗データが見つかりません: ${courseId}`);
+ return false;
+ }
+
+ // レッスンが属するモジュールを見つける
+ let targetModule = null;
+ for (const module of course.modules) {
+ if (module.lessons.includes(lessonId)) {
+ targetModule = module;
+ break;
+ }
+ }
+
+ if (!targetModule) {
+ console.warn(`レッスン ${lessonId} が属するモジュールが見つかりません`);
+ return false;
+ }
+
+ // 前提条件モジュールがすべて完了しているかチェック
+ for (const prerequisiteModuleId of targetModule.prerequisites) {
+ if (!progress.completedModules.includes(prerequisiteModuleId)) {
+ console.log(`前提条件モジュール ${prerequisiteModuleId} が未完了のため、レッスン ${lessonId} はロックされています`);
+ return false;
+ }
+ }
+
+ // モジュール内での順序チェック
+ const lessonIndex = targetModule.lessons.indexOf(lessonId);
+ if (lessonIndex > 0) {
+ // 前のレッスンが完了しているかチェック
+ const previousLessonId = targetModule.lessons[lessonIndex - 1];
+ if (!progress.completedLessons.includes(previousLessonId)) {
+ console.log(`前のレッスン ${previousLessonId} が未完了のため、レッスン ${lessonId} はロックされています`);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * モジュールがアンロックされているかチェック
+ */
+ isModuleUnlocked(courseId, moduleId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) {
+ console.warn(`コースまたは進捗データが見つかりません: ${courseId}`);
+ return false;
+ }
+
+ // 対象モジュールを見つける
+ const targetModule = course.modules.find(module => module.id === moduleId);
+ if (!targetModule) {
+ console.warn(`モジュール ${moduleId} が見つかりません`);
+ return false;
+ }
+
+ // 前提条件モジュールがすべて完了しているかチェック
+ for (const prerequisiteModuleId of targetModule.prerequisites) {
+ if (!progress.completedModules.includes(prerequisiteModuleId)) {
+ console.log(`前提条件モジュール ${prerequisiteModuleId} が未完了のため、モジュール ${moduleId} はロックされています`);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * アンロックされていないレッスンへのアクセス制御
+ */
+ enforceAccessControl(courseId, lessonId) {
+ if (!this.isLessonUnlocked(courseId, lessonId)) {
+ const course = this.getCourse(courseId);
+ const courseName = course ? course.title : courseId;
+
+ throw new Error(`レッスン ${lessonId} はまだアンロックされていません。前のレッスンを完了してください。(コース: ${courseName})`);
+ }
+ return true;
+ }
+
+ /**
+ * レッスンアクセス試行(制御付き)
+ */
+ attemptLessonAccess(courseId, lessonId) {
+ try {
+ this.enforceAccessControl(courseId, lessonId);
+ console.log(`レッスンアクセス許可: ${lessonId}`);
+ return {
+ success: true,
+ lessonId: lessonId,
+ message: 'レッスンにアクセスできます'
+ };
+ } catch (error) {
+ console.warn(`レッスンアクセス拒否: ${error.message}`);
+ return {
+ success: false,
+ lessonId: lessonId,
+ error: error.message,
+ suggestedAction: this.getSuggestedAction(courseId, lessonId)
+ };
+ }
+ }
+
+ /**
+ * アクセス拒否時の推奨アクションを取得
+ */
+ getSuggestedAction(courseId, lessonId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) {
+ return '進捗データを確認してください';
+ }
+
+ // レッスンが属するモジュールを見つける
+ let targetModule = null;
+ for (const module of course.modules) {
+ if (module.lessons.includes(lessonId)) {
+ targetModule = module;
+ break;
+ }
+ }
+
+ if (!targetModule) {
+ return 'コースデータを確認してください';
+ }
+
+ // 前提条件モジュールの確認
+ for (const prerequisiteModuleId of targetModule.prerequisites) {
+ if (!progress.completedModules.includes(prerequisiteModuleId)) {
+ const prerequisiteModule = course.modules.find(m => m.id === prerequisiteModuleId);
+ const moduleName = prerequisiteModule ? prerequisiteModule.title : prerequisiteModuleId;
+ return `まず「${moduleName}」モジュールを完了してください`;
+ }
+ }
+
+ // モジュール内での順序確認
+ const lessonIndex = targetModule.lessons.indexOf(lessonId);
+ if (lessonIndex > 0) {
+ const previousLessonId = targetModule.lessons[lessonIndex - 1];
+ if (!progress.completedLessons.includes(previousLessonId)) {
+ return `まず前のレッスン「${previousLessonId}」を完了してください`;
+ }
+ }
+
+ return '前提条件を確認してください';
+ }
+
+ /**
+ * 特定のコースのチャレンジデータを取得
+ */
+ getCourseChallenge(courseId, challengeId) {
+ const challenges = this.challengeData[courseId] || [];
+ return challenges.find(challenge => challenge.id === challengeId);
+ }
+
+ /**
+ * 現在のコースのチャレンジデータを取得
+ */
+ getCurrentCourseChallenge(challengeId) {
+ if (!this.currentCourse) return null;
+ return this.getCourseChallenge(this.currentCourse.id, challengeId);
+ }
+
+ /**
+ * コースの進捗を初期化
+ */
+ initializeCourseProgress(courseId) {
+ return this.progressManager.initializeCourseProgress(courseId);
+ }
+
+ /**
+ * モジュール完了をチェック
+ */
+ checkModuleCompletion(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return [];
+
+ const newlyCompletedModules = [];
+
+ course.modules.forEach(module => {
+ // モジュール内の全レッスンが完了しているかチェック
+ const allLessonsCompleted = module.lessons.every(lessonId =>
+ progress.completedLessons.includes(lessonId)
+ );
+
+ if (allLessonsCompleted && !progress.completedModules.includes(module.id)) {
+ this.progressManager.markModuleCompleted(courseId, module.id);
+ newlyCompletedModules.push({
+ moduleId: module.id,
+ moduleTitle: module.title,
+ moduleDescription: module.description,
+ completedLessons: module.lessons,
+ conceptSummary: this.generateModuleConceptSummary(courseId, module.id)
+ });
+ console.log(`モジュール完了: ${module.title}`);
+ }
+ });
+
+ return newlyCompletedModules;
+ }
+
+ /**
+ * コース完了をチェック
+ */
+ checkCourseCompletion(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return false;
+
+ // 全モジュールが完了しているかチェック
+ const allModulesCompleted = course.modules.every(module =>
+ progress.completedModules.includes(module.id)
+ );
+
+ if (allModulesCompleted && !progress.isCompleted) {
+ // コース完了を記録
+ this.progressManager.markCourseCompleted(courseId);
+
+ // コース完了処理を実行
+ const completionResult = this.processCourseCompletion(courseId);
+
+ console.log(`コース完了: ${course.title}`);
+ return completionResult;
+ }
+
+ return false;
+ }
+
+ /**
+ * コース完了処理を実行
+ */
+ processCourseCompletion(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return null;
+
+ // 完了統計を計算
+ const completionStats = this.calculateCourseCompletionStats(courseId);
+
+ // 次のコース推奨を生成
+ const recommendedCourses = this.generateCourseRecommendations(courseId);
+
+ // 完了証明書データを生成
+ const certificate = this.generateCourseCertificate(courseId, completionStats);
+
+ const completionResult = {
+ courseId: courseId,
+ courseTitle: course.title,
+ completedAt: new Date().toISOString(),
+ stats: completionStats,
+ certificate: certificate,
+ recommendedCourses: recommendedCourses,
+ achievements: this.generateAchievements(courseId, completionStats)
+ };
+
+ // 完了イベントを発火
+ this.triggerCourseCompletionEvent(completionResult);
+
+ return completionResult;
+ }
+
+ /**
+ * コース完了統計を計算
+ */
+ calculateCourseCompletionStats(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return null;
+
+ const totalLessons = course.modules.reduce((total, module) => total + module.lessons.length, 0);
+ const totalModules = course.modules.length;
+
+ const startDate = new Date(progress.startDate);
+ const completedDate = new Date();
+ const studyDuration = Math.ceil((completedDate - startDate) / (1000 * 60 * 60 * 24)); // 日数
+
+ return {
+ totalLessons: totalLessons,
+ totalModules: totalModules,
+ totalScore: progress.totalScore,
+ averageScore: totalLessons > 0 ? Math.round(progress.totalScore / totalLessons) : 0,
+ studyDuration: studyDuration,
+ startDate: progress.startDate,
+ completedDate: completedDate.toISOString(),
+ efficiency: this.calculateStudyEfficiency(studyDuration, course.estimatedHours)
+ };
+ }
+
+ /**
+ * 学習効率を計算
+ */
+ calculateStudyEfficiency(actualDays, estimatedHours) {
+ // 推定時間を日数に変換(1日2時間学習と仮定)
+ const estimatedDays = Math.ceil(estimatedHours / 2);
+
+ if (actualDays <= estimatedDays) {
+ return 'excellent'; // 優秀
+ } else if (actualDays <= estimatedDays * 1.5) {
+ return 'good'; // 良好
+ } else if (actualDays <= estimatedDays * 2) {
+ return 'average'; // 平均
+ } else {
+ return 'needs_improvement'; // 要改善
+ }
+ }
+
+ /**
+ * 次のコース推奨を生成
+ */
+ generateCourseRecommendations(completedCourseId) {
+ const allCourses = this.getCourses();
+ const completedCourse = this.getCourse(completedCourseId);
+
+ if (!completedCourse) return [];
+
+ // コース推奨ロジック
+ const recommendations = [];
+
+ // 難易度ベースの推奨
+ const difficultyProgression = {
+ 'sql-basics': ['db-fundamentals'],
+ 'db-fundamentals': ['big-data-basics'],
+ 'big-data-basics': []
+ };
+
+ const nextCourseIds = difficultyProgression[completedCourseId] || [];
+
+ nextCourseIds.forEach(courseId => {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (course && (!progress || !progress.isCompleted)) {
+ recommendations.push({
+ courseId: course.id,
+ title: course.title,
+ description: course.description,
+ difficulty: course.difficulty,
+ estimatedHours: course.estimatedHours,
+ reason: this.getRecommendationReason(completedCourseId, courseId),
+ priority: 'high'
+ });
+ }
+ });
+
+ // 関連コースの推奨
+ allCourses.forEach(course => {
+ if (course.id !== completedCourseId && !nextCourseIds.includes(course.id)) {
+ const progress = this.getCourseProgress(course.id);
+ if (!progress || !progress.isCompleted) {
+ recommendations.push({
+ courseId: course.id,
+ title: course.title,
+ description: course.description,
+ difficulty: course.difficulty,
+ estimatedHours: course.estimatedHours,
+ reason: '関連スキルの習得',
+ priority: 'medium'
+ });
+ }
+ }
+ });
+
+ return recommendations.slice(0, 3); // 最大3つの推奨
+ }
+
+ /**
+ * 推奨理由を取得
+ */
+ getRecommendationReason(completedCourseId, recommendedCourseId) {
+ const reasons = {
+ 'sql-basics': {
+ 'db-fundamentals': 'SQL基礎を習得したので、次はデータベース設計と操作を学びましょう',
+ 'big-data-basics': '大規模データ処理の基礎を学ぶ準備ができています'
+ },
+ 'db-fundamentals': {
+ 'big-data-basics': 'データベース基礎を習得したので、大規模データ処理技術を学びましょう',
+ 'sql-basics': 'SQL基礎で実践的なクエリ技術を身につけましょう'
+ },
+ 'big-data-basics': {
+ 'sql-basics': 'SQL基礎で基本的なクエリ技術を復習しましょう',
+ 'db-fundamentals': 'データベース設計の基礎を学びましょう'
+ }
+ };
+
+ return reasons[completedCourseId]?.[recommendedCourseId] || 'スキルアップのための次のステップ';
+ }
+
+ /**
+ * コース完了証明書を生成
+ */
+ generateCourseCertificate(courseId, stats) {
+ const course = this.getCourse(courseId);
+ if (!course) return null;
+
+ return {
+ certificateId: `cert-${courseId}-${Date.now()}`,
+ courseTitle: course.title,
+ courseDescription: course.description,
+ difficulty: course.difficulty,
+ estimatedHours: course.estimatedHours,
+ completedAt: stats.completedDate,
+ studyDuration: stats.studyDuration,
+ totalScore: stats.totalScore,
+ averageScore: stats.averageScore,
+ efficiency: stats.efficiency,
+ skills: this.getCourseSkills(courseId),
+ issuer: 'SQL学習プラットフォーム',
+ validationCode: this.generateValidationCode(courseId, stats.completedDate)
+ };
+ }
+
+ /**
+ * コースで習得したスキルを取得
+ */
+ getCourseSkills(courseId) {
+ const skillsMap = {
+ 'sql-basics': [
+ 'SELECT文の基本構文',
+ 'WHERE句による条件指定',
+ 'ORDER BYとLIMITによるデータ操作',
+ '集約関数(COUNT、SUM、AVG)',
+ 'GROUP BYとHAVING',
+ 'テーブル結合(JOIN)',
+ 'サブクエリの活用'
+ ],
+ 'db-fundamentals': [
+ 'DDL(データ定義言語)',
+ 'DML(データ操作言語)',
+ 'テーブル設計と制約',
+ 'インデックス管理',
+ 'トランザクション制御',
+ 'データ整合性管理'
+ ],
+ 'big-data-basics': [
+ 'DuckDBの特性理解',
+ '大規模データ読み込み',
+ 'ウィンドウ関数',
+ 'パフォーマンス最適化',
+ 'ETLパイプライン構築',
+ '分析用SQL技術'
+ ]
+ };
+
+ return skillsMap[courseId] || [];
+ }
+
+ /**
+ * 検証コードを生成
+ */
+ generateValidationCode(courseId, completedDate) {
+ const data = `${courseId}-${completedDate}`;
+ // 簡単なハッシュ生成(実際の実装では暗号化ライブラリを使用)
+ let hash = 0;
+ for (let i = 0; i < data.length; i++) {
+ const char = data.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // 32bit整数に変換
+ }
+ return Math.abs(hash).toString(16).toUpperCase().substring(0, 8);
+ }
+
+ /**
+ * 達成バッジを生成
+ */
+ generateAchievements(courseId, stats) {
+ const achievements = [];
+
+ // 基本完了バッジ
+ achievements.push({
+ id: `completion-${courseId}`,
+ title: `${this.getCourse(courseId)?.title} 完了`,
+ description: 'コースを完了しました',
+ icon: '🎓',
+ earnedAt: stats.completedDate
+ });
+
+ // 効率性バッジ
+ if (stats.efficiency === 'excellent') {
+ achievements.push({
+ id: `efficiency-${courseId}`,
+ title: '効率的学習者',
+ description: '予定より早くコースを完了しました',
+ icon: '⚡',
+ earnedAt: stats.completedDate
+ });
+ }
+
+ // 高得点バッジ
+ if (stats.averageScore >= 90) {
+ achievements.push({
+ id: `high-score-${courseId}`,
+ title: '高得点達成',
+ description: '平均90点以上を獲得しました',
+ icon: '🌟',
+ earnedAt: stats.completedDate
+ });
+ }
+
+ // 継続学習バッジ
+ if (stats.studyDuration <= 7) {
+ achievements.push({
+ id: `consistent-${courseId}`,
+ title: '集中学習者',
+ description: '1週間以内にコースを完了しました',
+ icon: '🔥',
+ earnedAt: stats.completedDate
+ });
+ }
+
+ return achievements;
+ }
+
+ /**
+ * コース完了イベントを発火
+ */
+ triggerCourseCompletionEvent(completionResult) {
+ // カスタムイベントを発火
+ const event = new CustomEvent('courseCompleted', {
+ detail: completionResult
+ });
+ document.dispatchEvent(event);
+
+ // UIControllerに通知
+ if (window.uiController && typeof window.uiController.onCourseCompleted === 'function') {
+ window.uiController.onCourseCompleted(completionResult);
+ }
+
+ // CourseUIに通知
+ if (window.courseUI && typeof window.courseUI.onCourseCompleted === 'function') {
+ window.courseUI.onCourseCompleted(completionResult);
+ }
+ }
+
+ /**
+ * モジュール完了時の概念要約を生成
+ */
+ generateModuleConceptSummary(courseId, moduleId) {
+ const course = this.getCourse(courseId);
+ if (!course) return null;
+
+ const module = course.modules.find(m => m.id === moduleId);
+ if (!module) return null;
+
+ // コースとモジュールに基づいて概念要約を生成
+ const conceptSummaries = {
+ 'sql-basics': {
+ 'module-1': {
+ title: 'SELECT文の基本',
+ keyConcepts: [
+ 'SELECT文の基本構文',
+ 'テーブルからのデータ取得',
+ '列の指定と全列取得(*)',
+ 'データベースの基本概念'
+ ],
+ practicalSkills: [
+ '基本的なSELECT文を書くことができる',
+ 'テーブルから必要な列のデータを取得できる',
+ 'SQLの基本的な実行方法を理解している'
+ ],
+ nextSteps: 'データの絞り込み(WHERE句)を学習して、より具体的な条件でデータを取得する方法を身につけましょう。'
+ },
+ 'module-2': {
+ title: 'データの絞り込み',
+ keyConcepts: [
+ 'WHERE句の使用方法',
+ '比較演算子(=, >, <, >=, <=, <>)',
+ '論理演算子(AND, OR, NOT)',
+ 'LIKE演算子とワイルドカード',
+ 'NULL値の扱い(IS NULL, IS NOT NULL)'
+ ],
+ practicalSkills: [
+ '条件を指定してデータを絞り込むことができる',
+ '複数の条件を組み合わせることができる',
+ '文字列の部分一致検索ができる'
+ ],
+ nextSteps: 'データの並び替えと制限を学習して、結果をより見やすく整理する方法を学びましょう。'
+ },
+ 'module-3': {
+ title: 'データの並び替えと制限',
+ keyConcepts: [
+ 'ORDER BY句による並び替え',
+ '昇順(ASC)と降順(DESC)',
+ '複数列による並び替え',
+ 'LIMIT句による結果数の制限',
+ 'OFFSET句による開始位置の指定'
+ ],
+ practicalSkills: [
+ 'データを任意の順序で並び替えることができる',
+ '必要な件数だけデータを取得できる',
+ 'ページング処理の基本を理解している'
+ ],
+ nextSteps: '集約関数を学習して、データの合計や平均などの統計情報を取得する方法を身につけましょう。'
+ },
+ 'module-4': {
+ title: '集約関数の基本',
+ keyConcepts: [
+ 'COUNT関数による件数の取得',
+ 'SUM関数による合計値の計算',
+ 'AVG関数による平均値の計算',
+ 'MAX/MIN関数による最大値・最小値の取得',
+ 'NULL値の集約関数での扱い'
+ ],
+ practicalSkills: [
+ 'データの件数を数えることができる',
+ '数値データの合計・平均を計算できる',
+ 'データの最大値・最小値を取得できる'
+ ],
+ nextSteps: 'グループ化と集計を学習して、カテゴリ別の統計情報を取得する方法を学びましょう。'
+ },
+ 'module-5': {
+ title: 'グループ化と集計',
+ keyConcepts: [
+ 'GROUP BY句によるデータのグループ化',
+ 'グループ別の集約関数の適用',
+ 'HAVING句による集約結果の絞り込み',
+ 'WHEREとHAVINGの違い',
+ 'グループ化における注意点'
+ ],
+ practicalSkills: [
+ 'カテゴリ別にデータを集計できる',
+ '集計結果に条件を適用できる',
+ '複雑な集計クエリを作成できる'
+ ],
+ nextSteps: 'テーブル結合を学習して、複数のテーブルからデータを組み合わせる方法を身につけましょう。'
+ },
+ 'module-6': {
+ title: 'テーブル結合の基本',
+ keyConcepts: [
+ 'INNER JOINによる内部結合',
+ 'LEFT JOINによる左外部結合',
+ 'RIGHT JOINによる右外部結合',
+ 'FULL OUTER JOINによる完全外部結合',
+ '結合条件の指定方法',
+ 'テーブルエイリアスの使用'
+ ],
+ practicalSkills: [
+ '複数のテーブルからデータを結合できる',
+ '適切な結合タイプを選択できる',
+ '結合条件を正しく指定できる'
+ ],
+ nextSteps: 'サブクエリと応用技術を学習して、より複雑なデータ分析を行う方法を学びましょう。'
+ },
+ 'module-7': {
+ title: 'サブクエリと応用',
+ keyConcepts: [
+ 'サブクエリの基本概念',
+ 'WHERE句でのサブクエリ',
+ 'FROM句でのサブクエリ',
+ 'SELECT句でのサブクエリ',
+ 'EXISTS演算子の使用',
+ 'IN演算子とサブクエリ'
+ ],
+ practicalSkills: [
+ '複雑な条件でデータを抽出できる',
+ 'サブクエリを使った高度な分析ができる',
+ '実践的なビジネス課題を解決できる'
+ ],
+ nextSteps: 'SQL基礎コースを完了しました!より高度なデータベース操作を学ぶためにDB基礎コースに進むことをお勧めします。'
+ }
+ },
+ 'db-fundamentals': {
+ 'module-1': {
+ title: 'DDL(データ定義言語)',
+ keyConcepts: [
+ 'CREATE TABLE文によるテーブル作成',
+ 'データ型の選択と指定',
+ 'ALTER TABLE文によるテーブル構造の変更',
+ 'DROP TABLE文によるテーブル削除',
+ '制約の基本概念'
+ ],
+ practicalSkills: [
+ 'テーブルを設計・作成できる',
+ 'テーブル構造を変更できる',
+ '適切なデータ型を選択できる'
+ ],
+ nextSteps: 'DML(データ操作言語)を学習して、作成したテーブルにデータを挿入・更新・削除する方法を学びましょう。'
+ },
+ 'module-2': {
+ title: 'DML(データ操作言語)',
+ keyConcepts: [
+ 'INSERT文によるデータ挿入',
+ 'UPDATE文によるデータ更新',
+ 'DELETE文によるデータ削除',
+ '条件付きの更新・削除',
+ 'バッチ処理の考慮事項'
+ ],
+ practicalSkills: [
+ 'テーブルにデータを挿入できる',
+ '既存データを安全に更新できる',
+ '不要なデータを削除できる'
+ ],
+ nextSteps: '制約とインデックスを学習して、データの整合性とパフォーマンスを向上させる方法を学びましょう。'
+ },
+ 'module-3': {
+ title: '制約とインデックス',
+ keyConcepts: [
+ 'PRIMARY KEY制約',
+ 'FOREIGN KEY制約',
+ 'UNIQUE制約',
+ 'NOT NULL制約',
+ 'CHECK制約',
+ 'インデックスの作成と管理'
+ ],
+ practicalSkills: [
+ 'データの整合性を保つ制約を設定できる',
+ 'パフォーマンスを向上させるインデックスを作成できる',
+ 'データベース設計の品質を向上させることができる'
+ ],
+ nextSteps: 'トランザクション管理を学習して、データの一貫性を保つ高度な技術を身につけましょう。'
+ },
+ 'module-4': {
+ title: 'トランザクション管理',
+ keyConcepts: [
+ 'ACID特性の理解',
+ 'BEGIN/COMMIT/ROLLBACKの使用',
+ 'トランザクション分離レベル',
+ 'デッドロックの理解と対策',
+ '同時実行制御'
+ ],
+ practicalSkills: [
+ 'トランザクションを適切に管理できる',
+ 'データの一貫性を保つことができる',
+ '同時実行環境での問題を理解している'
+ ],
+ nextSteps: 'DB基礎コースを完了しました!大規模データ処理に興味がある場合は、大規模データ基礎コースに進むことをお勧めします。'
+ }
+ },
+ 'big-data-basics': {
+ 'module-1': {
+ title: 'DuckDBの特徴',
+ keyConcepts: [
+ 'DuckDBの分析特化アーキテクチャ',
+ 'カラムナーストレージの利点',
+ 'インメモリ処理の特性',
+ 'PostgreSQL互換性',
+ 'ファイル形式サポート'
+ ],
+ practicalSkills: [
+ 'DuckDBの特徴を理解している',
+ '分析用途での利点を説明できる',
+ '適切な使用場面を判断できる'
+ ],
+ nextSteps: '大規模データの読み込みを学習して、様々なファイル形式からデータを効率的に処理する方法を学びましょう。'
+ },
+ 'module-2': {
+ title: '大規模データの読み込み',
+ keyConcepts: [
+ 'CSVファイルの効率的な読み込み',
+ 'Parquetファイルの処理',
+ 'JSONファイルの扱い',
+ 'ストリーミング処理',
+ 'データ型の自動推論'
+ ],
+ practicalSkills: [
+ '大容量ファイルを効率的に読み込める',
+ '様々なファイル形式を扱える',
+ 'メモリ効率を考慮した処理ができる'
+ ],
+ nextSteps: '高度な集計とウィンドウ関数を学習して、複雑な分析処理を行う方法を身につけましょう。'
+ },
+ 'module-3': {
+ title: '高度な集計とウィンドウ関数',
+ keyConcepts: [
+ 'ウィンドウ関数の基本',
+ 'ROW_NUMBER, RANK, DENSE_RANK',
+ 'LAG, LEAD関数',
+ '移動平均の計算',
+ 'パーティション分割'
+ ],
+ practicalSkills: [
+ 'ウィンドウ関数を使った高度な分析ができる',
+ '時系列データの分析ができる',
+ 'ランキングや順位付けができる'
+ ],
+ nextSteps: 'パフォーマンス最適化を学習して、大規模データ処理を高速化する技術を学びましょう。'
+ },
+ 'module-4': {
+ title: 'パフォーマンス最適化',
+ keyConcepts: [
+ 'クエリ実行計画の理解',
+ 'インデックス戦略',
+ 'パーティショニング',
+ 'メモリ管理',
+ '並列処理の活用'
+ ],
+ practicalSkills: [
+ 'クエリのパフォーマンスを分析できる',
+ '最適化手法を適用できる',
+ '大規模データを効率的に処理できる'
+ ],
+ nextSteps: 'ETLパイプライン構築を学習して、実践的なデータ処理システムを構築する方法を学びましょう。'
+ },
+ 'module-5': {
+ title: 'ETLパイプライン構築',
+ keyConcepts: [
+ 'ETLプロセスの設計',
+ 'データ品質管理',
+ 'エラーハンドリング',
+ 'スケジューリング',
+ 'モニタリングとログ'
+ ],
+ practicalSkills: [
+ '実践的なETLパイプラインを構築できる',
+ 'データ品質を管理できる',
+ '運用を考慮したシステムを設計できる'
+ ],
+ nextSteps: '大規模データ基礎コースを完了しました!実際のプロジェクトでこれらの技術を活用してください。'
+ }
+ }
+ };
+
+ const courseConceptSummaries = conceptSummaries[courseId];
+ if (!courseConceptSummaries) return null;
+
+ return courseConceptSummaries[moduleId] || null;
+ }
+
+ /**
+ * モジュール完了時の処理を実行
+ */
+ processModuleCompletionWithSummary(courseId, completedModuleIds) {
+ if (!completedModuleIds || completedModuleIds.length === 0) return [];
+
+ const course = this.getCourse(courseId);
+ if (!course) return [];
+
+ const moduleCompletionResults = [];
+
+ completedModuleIds.forEach(moduleData => {
+ const moduleId = typeof moduleData === 'string' ? moduleData : moduleData.moduleId;
+ const module = course.modules.find(m => m.id === moduleId);
+
+ if (module) {
+ const conceptSummary = this.generateModuleConceptSummary(courseId, moduleId);
+
+ moduleCompletionResults.push({
+ moduleId: moduleId,
+ moduleTitle: module.title,
+ moduleDescription: module.description,
+ completedLessons: module.lessons,
+ conceptSummary: conceptSummary,
+ unlockedModules: this.checkAndUnlockNextModules(courseId, moduleId)
+ });
+
+ console.log(`モジュール完了処理完了: ${module.title}`);
+ }
+ });
+
+ return moduleCompletionResults;
+ }
+
+ /**
+ * モジュール完了時に次のモジュールをアンロック
+ */
+ checkAndUnlockNextModules(courseId, completedModuleId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return [];
+
+ const newlyUnlockedModules = [];
+
+ // 完了したモジュールを前提条件とするモジュールを探す
+ course.modules.forEach(module => {
+ // まだ完了していないモジュールのみチェック
+ if (!progress.completedModules.includes(module.id)) {
+ // 前提条件に完了したモジュールが含まれているかチェック
+ if (module.prerequisites.includes(completedModuleId)) {
+ // 全ての前提条件が満たされているかチェック
+ const allPrerequisitesMet = module.prerequisites.every(prereqId =>
+ progress.completedModules.includes(prereqId)
+ );
+
+ if (allPrerequisitesMet) {
+ newlyUnlockedModules.push({
+ moduleId: module.id,
+ moduleTitle: module.title,
+ moduleDescription: module.description,
+ firstLessonId: module.lessons[0],
+ unlockedBy: completedModuleId
+ });
+
+ console.log(`新しいモジュールがアンロックされました: ${module.title} (${completedModuleId}により)`);
+ }
+ }
+ }
+ });
+
+ return newlyUnlockedModules;
+ }
+
+ /**
+ * コース完了をチェック
+ */
+ checkCourseCompletion(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return;
+
+ // 全モジュールが完了しているかチェック
+ const allModulesCompleted = course.modules.every(module =>
+ progress.completedModules.includes(module.id)
+ );
+
+ if (allModulesCompleted && !progress.isCompleted) {
+ this.progressManager.markCourseCompleted(courseId);
+ console.log(`コース完了: ${course.title}`);
+ }
+ }
+
+ /**
+ * 選択されたコースを復元
+ */
+ restoreSelectedCourse() {
+ const selectedCourseId = this.progressManager.getSelectedCourse();
+ if (selectedCourseId) {
+ const course = this.getCourse(selectedCourseId);
+ if (course) {
+ this.currentCourse = course;
+ console.log(`選択されたコースを復元しました: ${course.title}`);
+ }
+ }
+ }
+
+ /**
+ * 進捗をリセット
+ */
+ resetProgress(courseId = null) {
+ if (courseId) {
+ this.progressManager.resetCourseProgress(courseId);
+ console.log(`コース進捗をリセットしました: ${courseId}`);
+ } else {
+ this.progressManager.resetAllProgress();
+ this.currentCourse = null;
+ console.log('全ての進捗をリセットしました');
+ }
+ }
+
+ /**
+ * 進捗統計を取得
+ */
+ getProgressStats(courseId) {
+ return this.progressManager.getProgressStats(courseId);
+ }
+
+ /**
+ * 進捗データの整合性をチェック
+ */
+ validateProgressIntegrity() {
+ return this.progressManager.validateDataIntegrity();
+ }
+
+ /**
+ * 初期化状態を確認
+ */
+ isInitialized() {
+ return this.initialized;
+ }
+
+ /**
+ * アンロック可能なレッスンの一覧を取得
+ */
+ getUnlockedLessons(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return [];
+
+ const unlockedLessons = [];
+
+ course.modules.forEach(module => {
+ // モジュールがアンロックされているかチェック
+ if (this.isModuleUnlocked(courseId, module.id)) {
+ module.lessons.forEach(lessonId => {
+ if (this.isLessonUnlocked(courseId, lessonId)) {
+ unlockedLessons.push({
+ lessonId: lessonId,
+ moduleId: module.id,
+ moduleTitle: module.title,
+ isCompleted: progress.completedLessons.includes(lessonId)
+ });
+ }
+ });
+ }
+ });
+
+ return unlockedLessons;
+ }
+
+ /**
+ * 次にアンロックされるレッスンの情報を取得
+ */
+ getNextUnlockInfo(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return null;
+
+ // 全レッスンを順番に確認
+ for (const module of course.modules) {
+ for (const lessonId of module.lessons) {
+ // まだ完了していないレッスンを探す
+ if (!progress.completedLessons.includes(lessonId)) {
+ if (this.isLessonUnlocked(courseId, lessonId)) {
+ // 既にアンロックされている場合は次のレッスン
+ continue;
+ } else {
+ // ロックされているレッスンの場合、アンロック条件を返す
+ return this.getUnlockRequirements(courseId, lessonId);
+ }
+ }
+ }
+ }
+
+ return null; // 全レッスン完了
+ }
+
+ /**
+ * レッスンのアンロック要件を取得
+ */
+ getUnlockRequirements(courseId, lessonId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return null;
+
+ // レッスンが属するモジュールを見つける
+ let targetModule = null;
+ for (const module of course.modules) {
+ if (module.lessons.includes(lessonId)) {
+ targetModule = module;
+ break;
+ }
+ }
+
+ if (!targetModule) return null;
+
+ const requirements = {
+ lessonId: lessonId,
+ moduleId: targetModule.id,
+ moduleTitle: targetModule.title,
+ missingPrerequisites: [],
+ missingPreviousLessons: []
+ };
+
+ // 前提条件モジュールのチェック
+ for (const prerequisiteModuleId of targetModule.prerequisites) {
+ if (!progress.completedModules.includes(prerequisiteModuleId)) {
+ const prerequisiteModule = course.modules.find(m => m.id === prerequisiteModuleId);
+ requirements.missingPrerequisites.push({
+ moduleId: prerequisiteModuleId,
+ moduleTitle: prerequisiteModule ? prerequisiteModule.title : prerequisiteModuleId
+ });
+ }
+ }
+
+ // 前のレッスンのチェック
+ const lessonIndex = targetModule.lessons.indexOf(lessonId);
+ if (lessonIndex > 0) {
+ const previousLessonId = targetModule.lessons[lessonIndex - 1];
+ if (!progress.completedLessons.includes(previousLessonId)) {
+ requirements.missingPreviousLessons.push(previousLessonId);
+ }
+ }
+
+ return requirements;
+ }
+
+ /**
+ * コース全体のアンロック状況を取得
+ */
+ getCourseUnlockStatus(courseId) {
+ const course = this.getCourse(courseId);
+ const progress = this.getCourseProgress(courseId);
+
+ if (!course || !progress) return null;
+
+ const moduleStatus = course.modules.map(module => {
+ const isUnlocked = this.isModuleUnlocked(courseId, module.id);
+ const isCompleted = progress.completedModules.includes(module.id);
+
+ const lessonStatus = module.lessons.map(lessonId => ({
+ lessonId: lessonId,
+ isUnlocked: this.isLessonUnlocked(courseId, lessonId),
+ isCompleted: progress.completedLessons.includes(lessonId)
+ }));
+
+ const completedLessons = lessonStatus.filter(l => l.isCompleted).length;
+ const unlockedLessons = lessonStatus.filter(l => l.isUnlocked).length;
+
+ return {
+ moduleId: module.id,
+ moduleTitle: module.title,
+ isUnlocked: isUnlocked,
+ isCompleted: isCompleted,
+ totalLessons: module.lessons.length,
+ completedLessons: completedLessons,
+ unlockedLessons: unlockedLessons,
+ lessons: lessonStatus,
+ prerequisites: module.prerequisites
+ };
+ });
+
+ return {
+ courseId: courseId,
+ courseTitle: course.title,
+ modules: moduleStatus,
+ totalModules: course.modules.length,
+ completedModules: progress.completedModules.length,
+ unlockedModules: moduleStatus.filter(m => m.isUnlocked).length
+ };
+ }
+}
+
+export { CourseManager };
\ No newline at end of file
diff --git a/js/course-ui.js b/js/course-ui.js
new file mode 100644
index 0000000..74cfa48
--- /dev/null
+++ b/js/course-ui.js
@@ -0,0 +1,859 @@
+/**
+ * CourseUI - コース選択UIの管理クラス
+ * コース選択画面の表示、コース情報の表示、コース選択処理を担当
+ */
+export class CourseUI {
+ constructor(courseManager, gameEngine) {
+ this.courseManager = courseManager;
+ this.gameEngine = gameEngine;
+ this.elements = {
+ courseSelectionScreen: document.getElementById('course-selection-screen'),
+ courseList: document.getElementById('course-list'),
+ appLayout: document.querySelector('.app-layout')
+ };
+ this.bindEvents();
+ }
+
+ /**
+ * イベントリスナーを設定
+ */
+ bindEvents() {
+ // 必要に応じてイベントリスナーを追加
+ }
+
+ /**
+ * コース選択画面を表示
+ */
+ showCourseSelection() {
+ if (!this.courseManager.isInitialized()) {
+ console.error('CourseManagerが初期化されていません');
+ return;
+ }
+
+ const courses = this.courseManager.getCourses();
+ this.renderCourseList(courses);
+
+ // コース選択画面を表示し、メインアプリを非表示
+ this.elements.courseSelectionScreen.classList.remove('hidden');
+ this.elements.appLayout.style.display = 'none';
+ }
+
+ /**
+ * コース選択画面を非表示
+ */
+ hideCourseSelection() {
+ this.elements.courseSelectionScreen.classList.add('hidden');
+ this.elements.appLayout.style.display = 'flex';
+ }
+
+ /**
+ * コース一覧を描画
+ */
+ renderCourseList(courses) {
+ if (!courses || courses.length === 0) {
+ this.elements.courseList.innerHTML = '推奨事項
+ ${report.recommendations.map(rec => + `• ${rec.title}: ${rec.description}
`
+ ).join('')}
+ 利用可能なコースがありません
'; + return; + } + + const courseCards = courses.map(course => this.createCourseCard(course)).join(''); + this.elements.courseList.innerHTML = courseCards; + + // コース選択ボタンのイベントリスナーを設定 + this.bindCourseSelectionEvents(); + } + + /** + * コースカードのHTMLを生成 + */ + createCourseCard(course) { + const isComingSoon = course.status === 'coming_soon'; + const progress = !isComingSoon ? this.courseManager.getCourseProgress(course.id) : null; + const progressPercentage = progress ? this.calculateProgressPercentage(course, progress) : 0; + const isStarted = progress && progress.completedLessons.length > 0; + + const courseIcon = this.getCourseIcon(course.id); + const difficultyClass = this.getDifficultyClass(course.difficulty); + + // モジュール情報を生成 + const modulesList = course.modules.map(module => { + const isCompleted = progress && progress.completedModules.includes(module.id); + return `${module.title}`; + }).join(''); + + return ` +
+
+ `;
+ }
+
+ /**
+ * コース選択ボタンのイベントリスナーを設定
+ */
+ bindCourseSelectionEvents() {
+ // コース選択ボタン
+ const selectButtons = document.querySelectorAll('.course-select-btn');
+ selectButtons.forEach(button => {
+ button.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const courseId = button.dataset.courseId;
+ this.selectCourse(courseId);
+ });
+ });
+
+ // コース情報ボタン
+ const infoButtons = document.querySelectorAll('.course-info-btn');
+ infoButtons.forEach(button => {
+ button.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const courseId = button.dataset.courseId;
+ this.showCourseInfo(courseId);
+ });
+ });
+
+ // コースカード全体のクリック
+ const courseCards = document.querySelectorAll('.course-card');
+ courseCards.forEach(card => {
+ card.addEventListener('click', () => {
+ const courseId = card.dataset.courseId;
+ this.selectCourse(courseId);
+ });
+ });
+ }
+
+ /**
+ * コースを選択して学習を開始
+ */
+ async selectCourse(courseId) {
+ try {
+ // コースを取得
+ const course = this.courseManager.getCourse(courseId);
+
+ // 準備中のコースかチェック
+ if (course.status === 'coming_soon') {
+ alert('このコースは現在準備中です。近日公開予定です。');
+ return;
+ }
+
+ // コースを選択
+ const selectedCourse = this.courseManager.selectCourse(courseId);
+ console.log(`コースを選択しました: ${selectedCourse.title}`);
+
+ // GameEngineにコースを設定
+ if (this.gameEngine && typeof this.gameEngine.setCourse === 'function') {
+ await this.gameEngine.setCourse(course);
+ }
+
+ // コース選択画面を非表示にしてメイン画面を表示
+ this.hideCourseSelection();
+
+ // UIControllerに現在のコースを通知
+ if (window.uiController && typeof window.uiController.onCourseSelected === 'function') {
+ window.uiController.onCourseSelected(course);
+ }
+
+ // ProgressUIに現在のコースを通知
+ if (window.progressUI && typeof window.progressUI.onCourseSelected === 'function') {
+ window.progressUI.onCourseSelected(course);
+ }
+
+ // チャレンジを更新
+ if (window.uiController && typeof window.uiController.updateChallenge === 'function') {
+ window.uiController.updateChallenge();
+ }
+
+ } catch (error) {
+ console.error('コース選択エラー:', error);
+ alert(`コース選択に失敗しました: ${error.message}`);
+ }
+ }
+
+ /**
+ * コース詳細情報を表示
+ */
+ showCourseInfo(courseId) {
+ const course = this.courseManager.getCourse(courseId);
+ if (!course) {
+ console.error(`コースが見つかりません: ${courseId}`);
+ return;
+ }
+
+ const progress = this.courseManager.getCourseProgress(courseId);
+ const progressPercentage = progress ? this.calculateProgressPercentage(course, progress) : 0;
+
+ // モジュール詳細情報を生成
+ const moduleDetails = course.modules.map(module => {
+ const isCompleted = progress && progress.completedModules.includes(module.id);
+ const completedLessons = progress ? module.lessons.filter(lessonId =>
+ progress.completedLessons.includes(lessonId)
+ ).length : 0;
+
+ return `
+
+
+
+ ${courseIcon}
+
+
+ ${course.title}
+ ${course.difficulty} + ${isComingSoon ? '準備中' : ''} +
+ ${course.description}
+ ${isComingSoon ? '
🚧 このコースは現在準備中です。近日公開予定です。' : ''} +
+
+ 🚧 このコースは現在準備中です。近日公開予定です。' : ''} +
+
+
+ ${!isComingSoon && progressPercentage > 0 ? `
+
+ 👥
+ ${course.targetAudience}
+
+
+ ⏱️
+ ${course.estimatedHours}時間
+
+
+
+ ` : ''}
+
+
+ 進捗
+ ${progressPercentage}%
+
+
+
+
+
+
+
+
+ 📚
+ モジュール (${course.modules.length})
+
+
+ ${modulesList}
+
+
+ ${isComingSoon ? `
+
+
+ ` : `
+
+
+ `}
+
+
+
+ `;
+ }).join('');
+
+ const infoHtml = `
+ ${module.title} ${isCompleted ? '✅' : ''}
+${module.description}
+
+ レッスン進捗: ${completedLessons}/${module.lessons.length}
+
+ ${module.prerequisites.length > 0 ?
+ `前提条件: ${module.prerequisites.join(', ')}
` :
+ ''
+ }
+
+
+ `;
+
+ // モーダルを表示
+ const modal = document.createElement('div');
+ modal.className = 'course-info-overlay';
+ modal.innerHTML = infoHtml;
+ document.body.appendChild(modal);
+
+ // モーダルのイベントリスナー
+ const closeBtn = modal.querySelector('.course-info-close');
+ const selectBtn = modal.querySelector('.course-select-btn');
+
+ closeBtn.addEventListener('click', () => {
+ document.body.removeChild(modal);
+ });
+
+ selectBtn.addEventListener('click', () => {
+ document.body.removeChild(modal);
+ this.selectCourse(courseId);
+ });
+
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ document.body.removeChild(modal);
+ }
+ });
+ }
+
+ /**
+ * 進捗パーセンテージを計算
+ */
+ calculateProgressPercentage(course, progress) {
+ if (!progress || !course.modules) return 0;
+
+ const totalLessons = course.modules.reduce((total, module) => total + module.lessons.length, 0);
+ const completedLessons = progress.completedLessons.length;
+
+ return totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0;
+ }
+
+ /**
+ * コースアイコンを取得
+ */
+ getCourseIcon(courseId) {
+ const icons = {
+ 'sql-basics': '📊',
+ 'db-fundamentals': '🗄️',
+ 'big-data-basics': '🚀'
+ };
+ return icons[courseId] || '📚';
+ }
+
+ /**
+ * 難易度クラスを取得
+ */
+ getDifficultyClass(difficulty) {
+ const difficultyMap = {
+ '初級': 'beginner',
+ '中級': 'intermediate',
+ '上級': 'advanced'
+ };
+ return difficultyMap[difficulty] || 'beginner';
+ }
+
+ /**
+ * 初回アクセス時の処理
+ */
+ handleFirstTimeAccess() {
+ // 選択されたコースがない場合はコース選択画面を表示
+ const currentCourse = this.courseManager.getCurrentCourse();
+ if (!currentCourse) {
+ this.showCourseSelection();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * コース切り替え処理
+ */
+ switchCourse() {
+ this.showCourseSelection();
+ }
+
+ /**
+ * コース完了時の処理
+ */
+ onCourseCompleted(completionResult) {
+ console.log('コース完了イベントを受信:', completionResult);
+ this.showCourseCompletionModal(completionResult);
+ }
+
+ /**
+ * コース完了モーダルを表示
+ */
+ showCourseCompletionModal(completionResult) {
+ const modal = this.createCourseCompletionModal(completionResult);
+ document.body.appendChild(modal);
+
+ // モーダル表示アニメーション
+ setTimeout(() => {
+ modal.classList.add('show');
+ }, 10);
+
+ // 自動的に祝福エフェクトを表示
+ this.showCelebrationEffect();
+ }
+
+ /**
+ * コース完了モーダルのHTMLを生成
+ */
+ createCourseCompletionModal(completionResult) {
+ const { courseTitle, stats, certificate, recommendedCourses, achievements } = completionResult;
+
+ // 達成バッジのHTML生成
+ const achievementBadges = achievements.map(achievement => `
+
+
+
+
+
+ ${course.title}
+ +
+
+
+
+
+
+ 対象者: ${course.targetAudience}
+難易度: ${course.difficulty}
+推定時間: ${course.estimatedHours}時間
+進捗: ${progressPercentage}%
+
+
+
+ コース概要
+${course.description}
+
+
+ モジュール詳細
+ ${moduleDetails} +
+
+
+
+
+ `).join('');
+
+ // 推奨コースのHTML生成
+ const recommendedCoursesHtml = recommendedCourses.length > 0 ? `
+ ${achievement.icon}
+
+
+ ${achievement.title}
+ ${achievement.description}
+
+
+ ` : '';
+
+ // 効率性の表示テキスト
+ const efficiencyText = {
+ 'excellent': '優秀',
+ 'good': '良好',
+ 'average': '平均',
+ 'needs_improvement': '要改善'
+ };
+
+ const modalHtml = `
+ 🎯 次のステップ
+
+ ${recommendedCourses.map(course => `
+
+
+
+ `).join('')}
+
+
+ ${course.title}
+ ${course.difficulty} +${course.description}
+${course.reason}
+
+ ⏱️ ${course.estimatedHours}時間
+ ${course.priority === 'high' ? '推奨' : '関連'}
+
+
+
+
+ `;
+
+ const modal = document.createElement('div');
+ modal.className = 'course-completion-container';
+ modal.innerHTML = modalHtml;
+
+ // イベントリスナーを設定
+ this.bindCompletionModalEvents(modal, completionResult);
+
+ return modal;
+ }
+
+ /**
+ * コース完了モーダルのイベントリスナーを設定
+ */
+ bindCompletionModalEvents(modal, completionResult) {
+ // 修了証明書表示ボタン
+ const certificateBtn = modal.querySelector('#view-certificate-btn');
+ if (certificateBtn) {
+ certificateBtn.addEventListener('click', () => {
+ this.showCourseCertificate(completionResult.certificate);
+ });
+ }
+
+ // 他のコースを見るボタン
+ const continueBtn = modal.querySelector('#continue-learning-btn');
+ if (continueBtn) {
+ continueBtn.addEventListener('click', () => {
+ this.closeCompletionModal(modal);
+ this.showCourseSelection();
+ });
+ }
+
+ // 閉じるボタン
+ const closeBtn = modal.querySelector('#close-completion-btn');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => {
+ this.closeCompletionModal(modal);
+ });
+ }
+
+ // 推奨コース選択ボタン
+ const recommendedBtns = modal.querySelectorAll('.recommended-course-btn');
+ recommendedBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const courseId = btn.dataset.courseId;
+ this.closeCompletionModal(modal);
+ this.selectCourse(courseId);
+ });
+ });
+
+ // オーバーレイクリックで閉じる
+ const overlay = modal.querySelector('.course-completion-overlay');
+ if (overlay) {
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) {
+ this.closeCompletionModal(modal);
+ }
+ });
+ }
+ }
+
+ /**
+ * コース完了モーダルを閉じる
+ */
+ closeCompletionModal(modal) {
+ modal.classList.remove('show');
+ setTimeout(() => {
+ if (modal.parentNode) {
+ document.body.removeChild(modal);
+ }
+ }, 300);
+ }
+
+ /**
+ * 修了証明書を表示
+ */
+ showCourseCertificate(certificate) {
+ const certificateModal = this.createCertificateModal(certificate);
+ document.body.appendChild(certificateModal);
+
+ setTimeout(() => {
+ certificateModal.classList.add('show');
+ }, 10);
+ }
+
+ /**
+ * 修了証明書モーダルのHTMLを生成
+ */
+ createCertificateModal(certificate) {
+ const completedDate = new Date(certificate.completedAt).toLocaleDateString('ja-JP');
+
+ const certificateHtml = `
+
+
+
+
+
+ 🎉
+ おめでとうございます!
+${courseTitle} を完了しました
+
+ 完了日: ${new Date(completionResult.completedAt).toLocaleDateString('ja-JP')}
+
+
+
+
+
+
+
+
+
+ ${achievements.length > 0 ? `
+ 📊 学習統計
+
+
+
+
+ ${stats.totalLessons}
+ 完了レッスン
+
+
+ ${stats.totalModules}
+ 完了モジュール
+
+
+ ${stats.averageScore}
+ 平均スコア
+
+
+ ${stats.studyDuration}
+ 学習日数
+
+
+ ${efficiencyText[stats.efficiency]}
+ 学習効率
+
+
+ ` : ''}
+
+
+ ${certificate.skills.length > 0 ? `
+ 🏆 獲得バッジ
+
+ ${achievementBadges}
+
+
+
+ ` : ''}
+
+
+ ${recommendedCoursesHtml}
+ 💡 習得スキル
+
+ ${certificate.skills.map(skill => `
+ ✓ ${skill}
+ `).join('')}
+
+
+
+
+
+
+
+
+ `;
+
+ const modal = document.createElement('div');
+ modal.className = 'certificate-container';
+ modal.innerHTML = certificateHtml;
+
+ // イベントリスナーを設定
+ this.bindCertificateModalEvents(modal, certificate);
+
+ return modal;
+ }
+
+ /**
+ * 修了証明書モーダルのイベントリスナーを設定
+ */
+ bindCertificateModalEvents(modal, certificate) {
+ // 閉じるボタン
+ const closeBtn = modal.querySelector('.certificate-close');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => {
+ this.closeCertificateModal(modal);
+ });
+ }
+
+ // ダウンロードボタン
+ const downloadBtn = modal.querySelector('#download-certificate-btn');
+ if (downloadBtn) {
+ downloadBtn.addEventListener('click', () => {
+ this.downloadCertificate(certificate);
+ });
+ }
+
+ // 共有ボタン
+ const shareBtn = modal.querySelector('#share-certificate-btn');
+ if (shareBtn) {
+ shareBtn.addEventListener('click', () => {
+ this.shareCertificate(certificate);
+ });
+ }
+
+ // オーバーレイクリックで閉じる
+ const overlay = modal.querySelector('.certificate-overlay');
+ if (overlay) {
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) {
+ this.closeCertificateModal(modal);
+ }
+ });
+ }
+ }
+
+ /**
+ * 修了証明書モーダルを閉じる
+ */
+ closeCertificateModal(modal) {
+ modal.classList.remove('show');
+ setTimeout(() => {
+ if (modal.parentNode) {
+ document.body.removeChild(modal);
+ }
+ }, 300);
+ }
+
+ /**
+ * 修了証明書をダウンロード
+ */
+ downloadCertificate(certificate) {
+ // 簡単なテキスト形式での証明書ダウンロード
+ const certificateText = `
+修了証明書
+Certificate of Completion
+
+コース名: ${certificate.courseTitle}
+完了日: ${new Date(certificate.completedAt).toLocaleDateString('ja-JP')}
+学習期間: ${certificate.studyDuration}日
+平均スコア: ${certificate.averageScore}点
+検証コード: ${certificate.validationCode}
+
+習得スキル:
+${certificate.skills.map(skill => `• ${skill}`).join('\n')}
+
+発行者: ${certificate.issuer}
+ `.trim();
+
+ const blob = new Blob([certificateText], { type: 'text/plain;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `certificate-${certificate.certificateId}.txt`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ console.log('修了証明書をダウンロードしました');
+ }
+
+ /**
+ * 修了証明書を共有
+ */
+ shareCertificate(certificate) {
+ const shareText = `${certificate.courseTitle}を完了しました!🎉\n平均スコア: ${certificate.averageScore}点\n検証コード: ${certificate.validationCode}`;
+
+ if (navigator.share) {
+ navigator.share({
+ title: '修了証明書',
+ text: shareText,
+ url: window.location.href
+ }).catch(err => console.log('共有エラー:', err));
+ } else {
+ // フォールバック: クリップボードにコピー
+ navigator.clipboard.writeText(shareText).then(() => {
+ alert('修了証明書の情報をクリップボードにコピーしました!');
+ }).catch(err => {
+ console.error('クリップボードコピーエラー:', err);
+ alert('共有機能は利用できません');
+ });
+ }
+ }
+
+ /**
+ * 祝福エフェクトを表示
+ */
+ showCelebrationEffect() {
+ // 簡単な祝福エフェクト(紙吹雪風)
+ const celebrationContainer = document.createElement('div');
+ celebrationContainer.className = 'celebration-effect';
+ celebrationContainer.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修了証明書
+Certificate of Completion
+
+
+
+
+ これは以下のコースを修了したことを証明します
+
+
+
+ ${certificate.courseTitle}
+
+
+
+ ${certificate.courseDescription}
+
+
+
+
+
+
+ 難易度:
+ ${certificate.difficulty}
+
+
+ 推定学習時間:
+ ${certificate.estimatedHours}時間
+
+
+ 実際の学習期間:
+ ${certificate.studyDuration}日
+
+
+ 平均スコア:
+ ${certificate.averageScore}点
+
+
+ 完了日:
+ ${completedDate}
+
+
+
+ 習得スキル
+
+ ${certificate.skills.map(skill => `
+
+ • ${skill}
+ `).join('')}
+
+
+
+
+ ${certificate.issuer}
+ 検証コード: ${certificate.validationCode}
+ 🏆
+
+
+
+
+ 🎉
+ 🎊
+ ✨
+ 🌟
+ 🎈
+ `;
+
+ document.body.appendChild(celebrationContainer);
+
+ // 3秒後に削除
+ setTimeout(() => {
+ if (celebrationContainer.parentNode) {
+ document.body.removeChild(celebrationContainer);
+ }
+ }, 3000);
+ }
+}
\ No newline at end of file
diff --git a/js/duckdb-manager.js b/js/duckdb-manager.js
index cca558e..2c2ac5a 100644
--- a/js/duckdb-manager.js
+++ b/js/duckdb-manager.js
@@ -140,6 +140,24 @@ export class DuckDBManager {
async executeQuery(sql) {
if (!this.isInitialized) {
+ // ErrorHandlerを使用してデータベースエラーを処理
+ if (window.errorHandler) {
+ const result = await window.errorHandler.handleError('DATABASE_ERROR',
+ new Error('データベースが初期化されていません'), {
+ operation: 'executeQuery',
+ sql: sql.substring(0, 100) // SQLの最初の100文字のみログ
+ });
+
+ if (result.success && result.action === 'offline_mode') {
+ // オフラインモードの場合は制限された結果を返す
+ return {
+ success: false,
+ error: 'オフラインモードのため、SQLクエリを実行できません',
+ offline: true
+ };
+ }
+ }
+
throw new Error('データベースが初期化されていません');
}
diff --git a/js/error-handler.js b/js/error-handler.js
new file mode 100644
index 0000000..837c283
--- /dev/null
+++ b/js/error-handler.js
@@ -0,0 +1,928 @@
+/**
+ * ErrorHandler - 統一されたエラーハンドリングシステム
+ * コースシステム全体のエラー処理とフォールバック機能を提供
+ */
+class ErrorHandler {
+ constructor() {
+ this.errorLog = [];
+ this.maxLogSize = 100;
+ this.fallbackStrategies = new Map();
+ this.userNotificationCallbacks = [];
+
+ // フォールバック戦略を初期化
+ this.initializeFallbackStrategies();
+
+ // グローバルエラーハンドラーを設定
+ this.setupGlobalErrorHandlers();
+ }
+
+ /**
+ * フォールバック戦略を初期化
+ */
+ initializeFallbackStrategies() {
+ // コースデータ読み込みエラーのフォールバック
+ this.fallbackStrategies.set('COURSE_LOAD_ERROR', {
+ handler: this.handleCourseLoadError.bind(this),
+ description: 'コースデータ読み込みエラー時のフォールバック'
+ });
+
+ // 進捗データ破損のフォールバック
+ this.fallbackStrategies.set('PROGRESS_DATA_CORRUPTION', {
+ handler: this.handleProgressDataCorruption.bind(this),
+ description: '進捗データ破損時の復旧処理'
+ });
+
+ // 不正なコース・レッスンアクセスのエラー処理
+ this.fallbackStrategies.set('INVALID_ACCESS', {
+ handler: this.handleInvalidAccess.bind(this),
+ description: '不正なアクセス試行時の処理'
+ });
+
+ // チャレンジデータ読み込みエラー
+ this.fallbackStrategies.set('CHALLENGE_LOAD_ERROR', {
+ handler: this.handleChallengeLoadError.bind(this),
+ description: 'チャレンジデータ読み込みエラー時の処理'
+ });
+
+ // データベース接続エラー
+ this.fallbackStrategies.set('DATABASE_ERROR', {
+ handler: this.handleDatabaseError.bind(this),
+ description: 'データベース接続・実行エラー時の処理'
+ });
+ }
+
+ /**
+ * グローバルエラーハンドラーを設定
+ */
+ setupGlobalErrorHandlers() {
+ // 未処理のPromise拒否をキャッチ
+ window.addEventListener('unhandledrejection', (event) => {
+ this.logError('UNHANDLED_PROMISE_REJECTION', event.reason, {
+ promise: event.promise,
+ timestamp: new Date().toISOString()
+ });
+
+ // 重要なエラーの場合はユーザーに通知
+ if (this.isCriticalError(event.reason)) {
+ this.notifyUser('システムエラーが発生しました。ページを再読み込みしてください。', 'error');
+ }
+ });
+
+ // 一般的なJavaScriptエラーをキャッチ
+ window.addEventListener('error', (event) => {
+ this.logError('JAVASCRIPT_ERROR', event.error, {
+ filename: event.filename,
+ lineno: event.lineno,
+ colno: event.colno,
+ timestamp: new Date().toISOString()
+ });
+ });
+ }
+
+ /**
+ * エラーをログに記録
+ * @param {string} type - エラータイプ
+ * @param {Error|string} error - エラーオブジェクトまたはメッセージ
+ * @param {Object} context - 追加のコンテキスト情報
+ */
+ logError(type, error, context = {}) {
+ const errorEntry = {
+ id: this.generateErrorId(),
+ type: type,
+ message: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : null,
+ context: context,
+ timestamp: new Date().toISOString(),
+ userAgent: navigator.userAgent,
+ url: window.location.href
+ };
+
+ this.errorLog.push(errorEntry);
+
+ // ログサイズを制限
+ if (this.errorLog.length > this.maxLogSize) {
+ this.errorLog.shift();
+ }
+
+ // コンソールにも出力
+ console.error(`[${type}] ${errorEntry.message}`, {
+ error: error,
+ context: context,
+ errorId: errorEntry.id
+ });
+
+ return errorEntry.id;
+ }
+
+ /**
+ * エラーIDを生成
+ */
+ generateErrorId() {
+ return `ERR_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ }
+
+ /**
+ * 重要なエラーかどうかを判定
+ */
+ isCriticalError(error) {
+ const criticalPatterns = [
+ /database.*not.*initialized/i,
+ /course.*manager.*not.*found/i,
+ /progress.*data.*corrupted/i,
+ /failed.*to.*fetch/i
+ ];
+
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ return criticalPatterns.some(pattern => pattern.test(errorMessage));
+ }
+
+ /**
+ * コースデータ読み込みエラーの処理
+ * @param {Error} error - 発生したエラー
+ * @param {Object} context - エラーコンテキスト
+ * @returns {Object} フォールバック結果
+ */
+ async handleCourseLoadError(error, context = {}) {
+ const errorId = this.logError('COURSE_LOAD_ERROR', error, context);
+
+ console.log('コースデータ読み込みエラーを処理中...', { errorId });
+
+ try {
+ // 1. キャッシュされたデータを確認
+ const cachedCourses = this.getCachedCourseData();
+ if (cachedCourses && cachedCourses.length > 0) {
+ console.log('キャッシュされたコースデータを使用します');
+ this.notifyUser('ネットワークエラーのため、キャッシュされたコースデータを使用しています。', 'warning');
+ return {
+ success: true,
+ data: cachedCourses,
+ source: 'cache',
+ errorId: errorId
+ };
+ }
+
+ // 2. デフォルトコースデータを生成
+ const defaultCourses = this.generateDefaultCourseData();
+
+ // 3. デフォルトデータをキャッシュに保存
+ this.setCachedCourseData(defaultCourses);
+
+ console.log('デフォルトコースデータを生成しました');
+ this.notifyUser('コースデータの読み込みに失敗したため、基本コースを使用しています。', 'info');
+
+ return {
+ success: true,
+ data: defaultCourses,
+ source: 'default',
+ errorId: errorId
+ };
+
+ } catch (fallbackError) {
+ const fallbackErrorId = this.logError('COURSE_LOAD_FALLBACK_ERROR', fallbackError, { originalErrorId: errorId });
+
+ this.notifyUser('コースデータの読み込みに失敗しました。ページを再読み込みしてください。', 'error');
+
+ return {
+ success: false,
+ error: fallbackError,
+ errorId: fallbackErrorId,
+ originalErrorId: errorId
+ };
+ }
+ }
+
+ /**
+ * 進捗データ破損時の復旧処理
+ * @param {Error} error - 発生したエラー
+ * @param {Object} context - エラーコンテキスト
+ * @returns {Object} 復旧結果
+ */
+ async handleProgressDataCorruption(error, context = {}) {
+ const errorId = this.logError('PROGRESS_DATA_CORRUPTION', error, context);
+
+ console.log('進捗データ破損を検出、復旧処理を開始...', { errorId });
+
+ try {
+ // 1. 破損したデータをバックアップ
+ const corruptedData = this.backupCorruptedProgressData();
+
+ // 2. 復旧可能なデータを抽出
+ const recoveredData = this.attemptProgressDataRecovery(corruptedData);
+
+ // 3. 新しい進捗データ構造を初期化
+ const freshProgressData = this.initializeFreshProgressData();
+
+ // 4. 復旧されたデータをマージ
+ const mergedData = this.mergeRecoveredProgressData(freshProgressData, recoveredData);
+
+ // 5. 復旧されたデータを保存
+ this.saveRecoveredProgressData(mergedData);
+
+ console.log('進捗データの復旧が完了しました', {
+ errorId,
+ recoveredCourses: Object.keys(recoveredData).length
+ });
+
+ this.notifyUser(
+ `進捗データの問題を修復しました。${Object.keys(recoveredData).length}個のコースの進捗を復旧できました。`,
+ 'success'
+ );
+
+ return {
+ success: true,
+ recoveredData: mergedData,
+ recoveredCourses: Object.keys(recoveredData).length,
+ errorId: errorId
+ };
+
+ } catch (recoveryError) {
+ const recoveryErrorId = this.logError('PROGRESS_RECOVERY_ERROR', recoveryError, { originalErrorId: errorId });
+
+ // 完全リセットを実行
+ const resetResult = this.performCompleteProgressReset();
+
+ this.notifyUser(
+ '進捗データの復旧に失敗したため、進捗をリセットしました。学習を最初から開始してください。',
+ 'warning'
+ );
+
+ return {
+ success: true,
+ action: 'complete_reset',
+ errorId: recoveryErrorId,
+ originalErrorId: errorId,
+ resetResult: resetResult
+ };
+ }
+ }
+
+ /**
+ * 不正なコース・レッスンアクセスの処理
+ * @param {Error} error - 発生したエラー
+ * @param {Object} context - エラーコンテキスト
+ * @returns {Object} 処理結果
+ */
+ handleInvalidAccess(error, context = {}) {
+ const errorId = this.logError('INVALID_ACCESS', error, context);
+
+ const { courseId, lessonId, userId, attemptedAction } = context;
+
+ console.log('不正なアクセス試行を検出', {
+ errorId,
+ courseId,
+ lessonId,
+ attemptedAction
+ });
+
+ // アクセス試行の詳細を分析
+ const accessAnalysis = this.analyzeAccessAttempt(context);
+
+ // 適切なリダイレクト先を決定
+ const redirectTarget = this.determineRedirectTarget(accessAnalysis);
+
+ // ユーザーへの説明メッセージを生成
+ const userMessage = this.generateAccessErrorMessage(accessAnalysis);
+
+ // 推奨アクションを生成
+ const suggestedActions = this.generateSuggestedActions(accessAnalysis);
+
+ this.notifyUser(userMessage, 'warning');
+
+ return {
+ success: false,
+ error: error.message,
+ errorId: errorId,
+ analysis: accessAnalysis,
+ redirectTarget: redirectTarget,
+ suggestedActions: suggestedActions,
+ userMessage: userMessage
+ };
+ }
+
+ /**
+ * チャレンジデータ読み込みエラーの処理
+ * @param {Error} error - 発生したエラー
+ * @param {Object} context - エラーコンテキスト
+ * @returns {Object} 処理結果
+ */
+ async handleChallengeLoadError(error, context = {}) {
+ const errorId = this.logError('CHALLENGE_LOAD_ERROR', error, context);
+
+ const { courseId, challengeFile } = context;
+
+ console.log('チャレンジデータ読み込みエラーを処理中...', {
+ errorId,
+ courseId,
+ challengeFile
+ });
+
+ try {
+ // 1. 代替チャレンジファイルを試行
+ const alternativeChallenge = await this.tryAlternativeChallengeFile(courseId, challengeFile);
+ if (alternativeChallenge) {
+ this.notifyUser('一部のチャレンジデータを代替ファイルから読み込みました。', 'info');
+ return {
+ success: true,
+ data: alternativeChallenge,
+ source: 'alternative',
+ errorId: errorId
+ };
+ }
+
+ // 2. 基本チャレンジデータを生成
+ const basicChallenges = this.generateBasicChallenges(courseId);
+
+ this.notifyUser(
+ `${courseId}コースの一部チャレンジが利用できないため、基本的な問題を提供しています。`,
+ 'warning'
+ );
+
+ return {
+ success: true,
+ data: basicChallenges,
+ source: 'generated',
+ errorId: errorId
+ };
+
+ } catch (fallbackError) {
+ const fallbackErrorId = this.logError('CHALLENGE_LOAD_FALLBACK_ERROR', fallbackError, { originalErrorId: errorId });
+
+ this.notifyUser('チャレンジデータの読み込みに失敗しました。', 'error');
+
+ return {
+ success: false,
+ error: fallbackError,
+ errorId: fallbackErrorId,
+ originalErrorId: errorId
+ };
+ }
+ }
+
+ /**
+ * データベースエラーの処理
+ * @param {Error} error - 発生したエラー
+ * @param {Object} context - エラーコンテキスト
+ * @returns {Object} 処理結果
+ */
+ async handleDatabaseError(error, context = {}) {
+ const errorId = this.logError('DATABASE_ERROR', error, context);
+
+ console.log('データベースエラーを処理中...', { errorId });
+
+ try {
+ // 1. データベース接続の再試行
+ const reconnectResult = await this.attemptDatabaseReconnection();
+ if (reconnectResult.success) {
+ this.notifyUser('データベース接続を復旧しました。', 'success');
+ return {
+ success: true,
+ action: 'reconnected',
+ errorId: errorId
+ };
+ }
+
+ // 2. オフラインモードへの切り替え
+ const offlineMode = this.enableOfflineMode();
+
+ this.notifyUser(
+ 'データベースに接続できないため、オフラインモードで動作しています。一部機能が制限されます。',
+ 'warning'
+ );
+
+ return {
+ success: true,
+ action: 'offline_mode',
+ offlineMode: offlineMode,
+ errorId: errorId
+ };
+
+ } catch (fallbackError) {
+ const fallbackErrorId = this.logError('DATABASE_FALLBACK_ERROR', fallbackError, { originalErrorId: errorId });
+
+ this.notifyUser('データベースエラーが解決できません。ページを再読み込みしてください。', 'error');
+
+ return {
+ success: false,
+ error: fallbackError,
+ errorId: fallbackErrorId,
+ originalErrorId: errorId
+ };
+ }
+ }
+
+ // ===== ユーティリティメソッド =====
+
+ /**
+ * キャッシュされたコースデータを取得
+ */
+ getCachedCourseData() {
+ try {
+ const cached = localStorage.getItem('cached_course_data');
+ if (cached) {
+ const data = JSON.parse(cached);
+ // キャッシュの有効期限をチェック(24時間)
+ const cacheAge = Date.now() - data.timestamp;
+ if (cacheAge < 24 * 60 * 60 * 1000) {
+ return data.courses;
+ }
+ }
+ } catch (error) {
+ console.warn('キャッシュデータの読み込みに失敗:', error);
+ }
+ return null;
+ }
+
+ /**
+ * コースデータをキャッシュに保存
+ */
+ setCachedCourseData(courses) {
+ try {
+ const cacheData = {
+ courses: courses,
+ timestamp: Date.now()
+ };
+ localStorage.setItem('cached_course_data', JSON.stringify(cacheData));
+ } catch (error) {
+ console.warn('キャッシュデータの保存に失敗:', error);
+ }
+ }
+
+ /**
+ * デフォルトコースデータを生成
+ */
+ generateDefaultCourseData() {
+ return [{
+ id: 'sql-basics',
+ title: 'SQL基礎コース(基本版)',
+ description: 'ネットワークエラーのため基本版を使用しています',
+ targetAudience: '初心者',
+ difficulty: '初級',
+ estimatedHours: 4,
+ modules: [{
+ id: 'module-1',
+ title: '基本操作',
+ description: 'SQL基本操作を学習します',
+ lessons: ['challenge-001', 'challenge-002'],
+ prerequisites: []
+ }]
+ }];
+ }
+
+ /**
+ * 破損した進捗データをバックアップ
+ */
+ backupCorruptedProgressData() {
+ try {
+ const corruptedData = localStorage.getItem('course_progress');
+ if (corruptedData) {
+ const backupKey = `corrupted_progress_backup_${Date.now()}`;
+ localStorage.setItem(backupKey, corruptedData);
+ console.log(`破損データをバックアップしました: ${backupKey}`);
+ return corruptedData;
+ }
+ } catch (error) {
+ console.warn('破損データのバックアップに失敗:', error);
+ }
+ return null;
+ }
+
+ /**
+ * 進捗データの復旧を試行
+ */
+ attemptProgressDataRecovery(corruptedData) {
+ const recoveredData = {};
+
+ if (!corruptedData) return recoveredData;
+
+ try {
+ // JSONパースを試行
+ const parsed = JSON.parse(corruptedData);
+
+ // 各コースの進捗データを検証・復旧
+ for (const [courseId, progressData] of Object.entries(parsed)) {
+ if (this.isValidProgressData(progressData)) {
+ recoveredData[courseId] = progressData;
+ } else {
+ // 部分的に復旧可能なデータを抽出
+ const partialData = this.extractValidProgressFields(progressData);
+ if (partialData) {
+ recoveredData[courseId] = partialData;
+ }
+ }
+ }
+ } catch (error) {
+ console.warn('進捗データの復旧に失敗:', error);
+ }
+
+ return recoveredData;
+ }
+
+ /**
+ * 進捗データの有効性を検証
+ */
+ isValidProgressData(data) {
+ return data &&
+ typeof data === 'object' &&
+ Array.isArray(data.completedLessons) &&
+ Array.isArray(data.completedModules) &&
+ typeof data.totalScore === 'number';
+ }
+
+ /**
+ * 有効な進捗フィールドを抽出
+ */
+ extractValidProgressFields(data) {
+ if (!data || typeof data !== 'object') return null;
+
+ const extracted = {
+ completedLessons: Array.isArray(data.completedLessons) ? data.completedLessons : [],
+ completedModules: Array.isArray(data.completedModules) ? data.completedModules : [],
+ totalScore: typeof data.totalScore === 'number' ? data.totalScore : 0,
+ isCompleted: Boolean(data.isCompleted),
+ startDate: data.startDate || new Date().toISOString(),
+ lastAccessed: new Date().toISOString()
+ };
+
+ return extracted;
+ }
+
+ /**
+ * 新しい進捗データ構造を初期化
+ */
+ initializeFreshProgressData() {
+ return {
+ courseProgress: {},
+ selectedCourse: null,
+ lastUpdated: new Date().toISOString()
+ };
+ }
+
+ /**
+ * 復旧されたデータをマージ
+ */
+ mergeRecoveredProgressData(freshData, recoveredData) {
+ freshData.courseProgress = { ...recoveredData };
+ return freshData;
+ }
+
+ /**
+ * 復旧されたデータを保存
+ */
+ saveRecoveredProgressData(data) {
+ try {
+ localStorage.setItem('course_progress', JSON.stringify(data.courseProgress));
+ console.log('復旧された進捗データを保存しました');
+ } catch (error) {
+ console.error('復旧データの保存に失敗:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * 完全な進捗リセットを実行
+ */
+ performCompleteProgressReset() {
+ try {
+ localStorage.removeItem('course_progress');
+ localStorage.removeItem('selected_course');
+ console.log('進捗データを完全にリセットしました');
+ return { success: true, action: 'complete_reset' };
+ } catch (error) {
+ console.error('進捗リセットに失敗:', error);
+ return { success: false, error: error.message };
+ }
+ }
+
+ /**
+ * アクセス試行を分析
+ */
+ analyzeAccessAttempt(context) {
+ const { courseId, lessonId, currentProgress } = context;
+
+ return {
+ courseId: courseId,
+ lessonId: lessonId,
+ hasValidCourse: Boolean(courseId),
+ hasValidLesson: Boolean(lessonId),
+ hasProgress: Boolean(currentProgress),
+ accessType: this.determineAccessType(context),
+ severity: this.determineAccessSeverity(context)
+ };
+ }
+
+ /**
+ * アクセスタイプを決定
+ */
+ determineAccessType(context) {
+ if (!context.courseId) return 'INVALID_COURSE';
+ if (!context.lessonId) return 'INVALID_LESSON';
+ if (context.lessonLocked) return 'LOCKED_LESSON';
+ return 'UNKNOWN_ACCESS_ERROR';
+ }
+
+ /**
+ * アクセスエラーの重要度を決定
+ */
+ determineAccessSeverity(context) {
+ if (context.attemptedAction === 'direct_url_access') return 'HIGH';
+ if (context.lessonLocked) return 'MEDIUM';
+ return 'LOW';
+ }
+
+ /**
+ * リダイレクト先を決定
+ */
+ determineRedirectTarget(analysis) {
+ switch (analysis.accessType) {
+ case 'INVALID_COURSE':
+ return { type: 'course_selection', message: 'コース選択画面に戻ります' };
+ case 'INVALID_LESSON':
+ return { type: 'course_overview', courseId: analysis.courseId, message: 'コース概要に戻ります' };
+ case 'LOCKED_LESSON':
+ return { type: 'last_available_lesson', message: '利用可能な最後のレッスンに移動します' };
+ default:
+ return { type: 'home', message: 'ホーム画面に戻ります' };
+ }
+ }
+
+ /**
+ * アクセスエラーメッセージを生成
+ */
+ generateAccessErrorMessage(analysis) {
+ switch (analysis.accessType) {
+ case 'INVALID_COURSE':
+ return '指定されたコースが見つかりません。利用可能なコースから選択してください。';
+ case 'INVALID_LESSON':
+ return '指定されたレッスンが見つかりません。コース内の利用可能なレッスンを確認してください。';
+ case 'LOCKED_LESSON':
+ return 'このレッスンはまだロックされています。前のレッスンを完了してからアクセスしてください。';
+ default:
+ return 'アクセスエラーが発生しました。正しい手順でレッスンにアクセスしてください。';
+ }
+ }
+
+ /**
+ * 推奨アクションを生成
+ */
+ generateSuggestedActions(analysis) {
+ const actions = [];
+
+ switch (analysis.accessType) {
+ case 'INVALID_COURSE':
+ actions.push({ action: 'select_course', label: 'コースを選択する' });
+ break;
+ case 'INVALID_LESSON':
+ actions.push({ action: 'view_course_overview', label: 'コース概要を見る' });
+ actions.push({ action: 'continue_from_last', label: '前回の続きから始める' });
+ break;
+ case 'LOCKED_LESSON':
+ actions.push({ action: 'complete_prerequisites', label: '前提レッスンを完了する' });
+ actions.push({ action: 'view_progress', label: '進捗を確認する' });
+ break;
+ }
+
+ actions.push({ action: 'contact_support', label: 'サポートに問い合わせる' });
+
+ return actions;
+ }
+
+ /**
+ * 代替チャレンジファイルを試行
+ */
+ async tryAlternativeChallengeFile(courseId, originalFile) {
+ const alternatives = [
+ 'slides/challenges.json', // デフォルトファイル
+ `slides/${courseId}-backup-challenges.json`,
+ 'slides/basic-challenges.json'
+ ];
+
+ for (const altFile of alternatives) {
+ if (altFile === originalFile) continue;
+
+ try {
+ const response = await fetch(altFile);
+ if (response.ok) {
+ const data = await response.json();
+ console.log(`代替チャレンジファイルを読み込みました: ${altFile}`);
+ return data;
+ }
+ } catch (error) {
+ console.warn(`代替ファイル読み込み失敗: ${altFile}`, error);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 基本チャレンジデータを生成
+ */
+ generateBasicChallenges(courseId) {
+ const basicChallenges = [
+ {
+ id: 'basic-001',
+ title: '基本的なSELECT文',
+ description: 'テーブルからデータを取得する基本的なクエリです。',
+ solution: 'SELECT * FROM customers LIMIT 5;',
+ expectedColumns: ['customer_id', 'company_name', 'contact_name'],
+ difficulty: 1,
+ hints: ['SELECT文の基本構文を使用してください', 'LIMIT句で結果数を制限できます']
+ }
+ ];
+
+ console.log(`${courseId}用の基本チャレンジを生成しました`);
+ return basicChallenges;
+ }
+
+ /**
+ * データベース再接続を試行
+ */
+ async attemptDatabaseReconnection() {
+ try {
+ if (window.dbManager && typeof window.dbManager.initialize === 'function') {
+ const result = await window.dbManager.initialize();
+ return { success: result };
+ }
+ return { success: false, reason: 'dbManager not available' };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ }
+
+ /**
+ * オフラインモードを有効化
+ */
+ enableOfflineMode() {
+ const offlineMode = {
+ enabled: true,
+ features: {
+ courseSelection: true,
+ progressTracking: true,
+ basicChallenges: true,
+ sqlExecution: false,
+ dataVisualization: false
+ },
+ limitations: [
+ 'SQLクエリの実行ができません',
+ 'データベースの結果表示ができません',
+ '一部の高度な機能が制限されます'
+ ]
+ };
+
+ // オフラインモード状態を保存
+ try {
+ localStorage.setItem('offline_mode', JSON.stringify(offlineMode));
+ } catch (error) {
+ console.warn('オフラインモード状態の保存に失敗:', error);
+ }
+
+ return offlineMode;
+ }
+
+ /**
+ * ユーザー通知コールバックを追加
+ */
+ addNotificationCallback(callback) {
+ if (typeof callback === 'function') {
+ this.userNotificationCallbacks.push(callback);
+ }
+ }
+
+ /**
+ * ユーザーに通知
+ */
+ notifyUser(message, type = 'info') {
+ // 登録されたコールバックを実行
+ this.userNotificationCallbacks.forEach(callback => {
+ try {
+ callback(message, type);
+ } catch (error) {
+ console.error('通知コールバックエラー:', error);
+ }
+ });
+
+ // デフォルトの通知方法(コンソール出力)
+ const logMethod = type === 'error' ? console.error :
+ type === 'warning' ? console.warn :
+ console.log;
+
+ logMethod(`[${type.toUpperCase()}] ${message}`);
+ }
+
+ /**
+ * エラーログを取得
+ */
+ getErrorLog(limit = 10) {
+ return this.errorLog.slice(-limit);
+ }
+
+ /**
+ * エラー統計を取得
+ */
+ getErrorStats() {
+ const stats = {};
+
+ this.errorLog.forEach(error => {
+ stats[error.type] = (stats[error.type] || 0) + 1;
+ });
+
+ return {
+ totalErrors: this.errorLog.length,
+ errorTypes: stats,
+ recentErrors: this.getErrorLog(5)
+ };
+ }
+
+ /**
+ * エラーハンドラーを実行
+ */
+ async handleError(errorType, error, context = {}) {
+ const strategy = this.fallbackStrategies.get(errorType);
+
+ if (strategy) {
+ console.log(`エラーハンドラーを実行: ${errorType}`, { error, context });
+ return await strategy.handler(error, context);
+ } else {
+ // 未知のエラータイプの場合は汎用処理
+ const errorId = this.logError('UNKNOWN_ERROR', error, { ...context, errorType });
+ this.notifyUser(`予期しないエラーが発生しました: ${error.message}`, 'error');
+
+ return {
+ success: false,
+ error: error.message,
+ errorId: errorId,
+ handled: false
+ };
+ }
+ }
+
+ /**
+ * システムの健全性をチェック
+ */
+ performHealthCheck() {
+ const healthStatus = {
+ timestamp: new Date().toISOString(),
+ components: {},
+ overall: 'healthy'
+ };
+
+ // CourseManager の状態チェック
+ if (window.courseManager) {
+ healthStatus.components.courseManager = {
+ status: window.courseManager.initialized ? 'healthy' : 'unhealthy',
+ coursesLoaded: window.courseManager.courses?.length || 0
+ };
+ } else {
+ healthStatus.components.courseManager = { status: 'missing' };
+ healthStatus.overall = 'degraded';
+ }
+
+ // データベースの状態チェック
+ if (window.dbManager) {
+ healthStatus.components.database = {
+ status: window.dbManager.isInitialized ? 'healthy' : 'unhealthy'
+ };
+ } else {
+ healthStatus.components.database = { status: 'missing' };
+ healthStatus.overall = 'degraded';
+ }
+
+ // LocalStorageの状態チェック
+ try {
+ localStorage.setItem('health_check', 'test');
+ localStorage.removeItem('health_check');
+ healthStatus.components.localStorage = { status: 'healthy' };
+ } catch (error) {
+ healthStatus.components.localStorage = {
+ status: 'unhealthy',
+ error: error.message
+ };
+ healthStatus.overall = 'degraded';
+ }
+
+ // エラー率のチェック
+ const recentErrors = this.errorLog.filter(error =>
+ Date.now() - new Date(error.timestamp).getTime() < 5 * 60 * 1000 // 5分以内
+ );
+
+ if (recentErrors.length > 5) {
+ healthStatus.overall = 'unhealthy';
+ healthStatus.components.errorRate = {
+ status: 'high',
+ recentErrors: recentErrors.length
+ };
+ } else {
+ healthStatus.components.errorRate = {
+ status: 'normal',
+ recentErrors: recentErrors.length
+ };
+ }
+
+ return healthStatus;
+ }
+}
+
+// グローバルインスタンスを作成
+window.errorHandler = new ErrorHandler();
+
+export { ErrorHandler };
\ No newline at end of file
diff --git a/js/game-engine.js b/js/game-engine.js
index ecda5e0..9bda8e0 100644
--- a/js/game-engine.js
+++ b/js/game-engine.js
@@ -7,6 +7,9 @@ export class GameEngine {
this.attempts = 0;
this.startTime = Date.now();
this.slideManager = null;
+ this.currentCourse = null;
+ this.courseManager = null;
+ this.adaptiveLearning = null;
}
setSlideManager(slideManager) {
@@ -39,6 +42,12 @@ export class GameEngine {
}
nextChallenge() {
+ // コースが設定されている場合はコース対応版を使用
+ if (this.currentCourse && this.courseManager) {
+ return this.courseAwareNextChallenge();
+ }
+
+ // 従来の動作
if (this.currentChallengeIndex < this.challenges.length - 1) {
this.currentChallengeIndex++;
this.attempts = 0;
@@ -49,6 +58,12 @@ export class GameEngine {
}
previousChallenge() {
+ // コースが設定されている場合はコース対応版を使用
+ if (this.currentCourse && this.courseManager) {
+ return this.courseAwarePreviousChallenge();
+ }
+
+ // 従来の動作
if (this.currentChallengeIndex > 0) {
this.currentChallengeIndex--;
this.attempts = 0;
@@ -58,50 +73,234 @@ export class GameEngine {
return false;
}
+ /**
+ * コース対応の次のチャレンジ
+ */
+ courseAwareNextChallenge() {
+ // 次に利用可能なチャレンジを取得
+ const nextAvailable = this.getNextAvailableChallenge();
+
+ if (nextAvailable) {
+ // アクセス制御をチェック
+ const accessResult = this.courseManager.attemptLessonAccess(
+ this.currentCourse.id,
+ nextAvailable.challenge.lessonId
+ );
+
+ if (accessResult.success) {
+ this.currentChallengeIndex = nextAvailable.index;
+ this.attempts = 0;
+ this.hintsUsed = 0;
+ this.startTime = Date.now();
+ console.log(`次のレッスンに移動: ${nextAvailable.challenge.lessonId}`);
+ return true;
+ } else {
+ console.warn(`次のレッスンへのアクセスが拒否されました: ${accessResult.error}`);
+ return false;
+ }
+ }
+
+ // 利用可能なチャレンジがない場合は従来の動作
+ if (this.currentChallengeIndex < this.challenges.length - 1) {
+ const nextIndex = this.currentChallengeIndex + 1;
+ const nextChallenge = this.challenges[nextIndex];
+
+ // レッスンIDがある場合はアクセス制御をチェック
+ if (nextChallenge.lessonId) {
+ const accessResult = this.courseManager.attemptLessonAccess(
+ this.currentCourse.id,
+ nextChallenge.lessonId
+ );
+
+ if (!accessResult.success) {
+ console.warn(`レッスンアクセス拒否: ${accessResult.error}`);
+ console.log(`推奨アクション: ${accessResult.suggestedAction}`);
+ return false;
+ }
+ }
+
+ this.currentChallengeIndex = nextIndex;
+ this.attempts = 0;
+ this.hintsUsed = 0;
+ this.startTime = Date.now();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * コース対応の前のチャレンジ
+ */
+ courseAwarePreviousChallenge() {
+ if (this.currentChallengeIndex > 0) {
+ // 前のチャレンジに移動
+ this.currentChallengeIndex--;
+ this.attempts = 0;
+ this.hintsUsed = 0;
+ this.startTime = Date.now();
+
+ // 前のチャレンジがアンロックされているかチェック
+ const challenge = this.getCurrentChallenge();
+ if (challenge.lessonId && !this.courseManager.isLessonUnlocked(this.currentCourse.id, challenge.lessonId)) {
+ // アンロックされていない場合は、アンロックされている最後のチャレンジを探す
+ for (let i = this.currentChallengeIndex; i >= 0; i--) {
+ const prevChallenge = this.challenges[i];
+ if (!prevChallenge.lessonId || this.courseManager.isLessonUnlocked(this.currentCourse.id, prevChallenge.lessonId)) {
+ this.currentChallengeIndex = i;
+ return true;
+ }
+ }
+
+ // アンロックされたチャレンジが見つからない場合は最初に戻る
+ this.currentChallengeIndex = 0;
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 前のチャレンジに移動可能かチェック
+ */
+ canGoPrevious() {
+ if (this.currentCourse && this.courseManager) {
+ return this.canGoPreviousCourseAware();
+ }
+
+ // 従来の動作
+ return this.currentChallengeIndex > 0;
+ }
+
+ /**
+ * 次のチャレンジに移動可能かチェック
+ */
+ canGoNext() {
+ if (this.currentCourse && this.courseManager) {
+ return this.canGoNextCourseAware();
+ }
+
+ // 従来の動作
+ return this.currentChallengeIndex < this.challenges.length - 1;
+ }
+
+ /**
+ * コース対応の前のチャレンジ移動可能チェック
+ */
+ canGoPreviousCourseAware() {
+ if (this.currentChallengeIndex <= 0) {
+ return false;
+ }
+
+ // 前のチャレンジがアンロックされているかチェック
+ for (let i = this.currentChallengeIndex - 1; i >= 0; i--) {
+ const challenge = this.challenges[i];
+ if (!challenge.lessonId || this.courseManager.isLessonUnlocked(this.currentCourse.id, challenge.lessonId)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * コース対応の次のチャレンジ移動可能チェック
+ */
+ canGoNextCourseAware() {
+ // 次に利用可能なチャレンジがあるかチェック
+ const nextAvailable = this.getNextAvailableChallenge();
+ if (nextAvailable) {
+ // アクセス制御をチェック
+ const accessResult = this.courseManager.attemptLessonAccess(
+ this.currentCourse.id,
+ nextAvailable.challenge.lessonId
+ );
+ return accessResult.success;
+ }
+
+ // 従来の方法でチェック
+ if (this.currentChallengeIndex < this.challenges.length - 1) {
+ const nextChallenge = this.challenges[this.currentChallengeIndex + 1];
+ if (nextChallenge.lessonId) {
+ return this.courseManager.isLessonUnlocked(this.currentCourse.id, nextChallenge.lessonId);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
checkAnswer(result) {
const challenge = this.getCurrentChallenge();
this.attempts++;
// スライドタイプの場合は常に正解
if (challenge.type === 'slide') {
+ const challengeData = {
+ correct: true,
+ score: 0,
+ attempts: this.attempts,
+ hintsUsed: this.hintsUsed,
+ timeSpent: Date.now() - this.startTime,
+ challenge: challenge
+ };
+ this.trackPerformanceData(challengeData);
+
return {
correct: true,
message: "スライドを確認しました!"
};
}
+ let isCorrect = false;
+ let errorType = null;
+ let message = "";
+
if (!result.success) {
- return {
- correct: false,
- message: `エラー: ${result.error}`
- };
- }
+ errorType = 'syntax';
+ message = `エラー: ${result.error}`;
+ } else if (!challenge.expectedColumns) {
+ isCorrect = true;
+ message = "正解です!";
+ } else {
+ // 列名チェック
+ const expectedCols = challenge.expectedColumns.sort();
+ const actualCols = result.columns.sort();
- // expectedColumnsが存在しない場合はスキップ
- if (!challenge.expectedColumns) {
- return {
- correct: true,
- message: "正解です!"
- };
+ if (JSON.stringify(expectedCols) !== JSON.stringify(actualCols)) {
+ errorType = 'column';
+ message = `期待される列: ${expectedCols.join(', ')}\n実際の列: ${actualCols.join(', ')}`;
+ } else {
+ isCorrect = true;
+ message = "正解です!";
+ }
}
- // 列名チェック
- const expectedCols = challenge.expectedColumns.sort();
- const actualCols = result.columns.sort();
+ // パフォーマンスデータを記録
+ const challengeData = {
+ correct: isCorrect,
+ score: isCorrect ? this.calculateScore() : 0,
+ attempts: this.attempts,
+ hintsUsed: this.hintsUsed,
+ timeSpent: Date.now() - this.startTime,
+ challenge: challenge,
+ errorType: errorType
+ };
- if (JSON.stringify(expectedCols) !== JSON.stringify(actualCols)) {
- return {
- correct: false,
- message: `期待される列: ${expectedCols.join(', ')}\n実際の列: ${actualCols.join(', ')}`
- };
- }
+ this.trackPerformanceData(challengeData);
- // 正解の場合
- this.calculateScore();
+ // 正解の場合のみ進捗を更新
+ if (isCorrect) {
+ this.onChallengeCompleted();
+ }
+
return {
- correct: true,
- message: "正解です!",
- score: this.score
+ correct: isCorrect,
+ message: message,
+ score: isCorrect ? challengeData.score : 0
};
}
@@ -115,6 +314,17 @@ export class GameEngine {
this.attempts++;
if (!userSQL || userSQL.trim() === '') {
+ const challengeData = {
+ correct: false,
+ score: 0,
+ attempts: this.attempts,
+ hintsUsed: this.hintsUsed,
+ timeSpent: Date.now() - this.startTime,
+ challenge: challenge,
+ errorType: 'incomplete'
+ };
+ this.trackPerformanceData(challengeData);
+
return {
correct: false,
message: "SQLを構築してください"
@@ -133,46 +343,79 @@ export class GameEngine {
window.dbManager.executeQuery(challenge.solution)
]);
+ let isCorrect = false;
+ let errorType = null;
+ let message = "";
+
// ユーザーのSQLでエラーが発生した場合
if (!userResult.success) {
- return {
- correct: false,
- message: `SQLエラー: ${userResult.error}`
- };
+ errorType = 'syntax';
+ message = `SQLエラー: ${userResult.error}`;
}
-
// 正解SQLでエラーが発生した場合(チャレンジデータの問題)
- if (!correctResult.success) {
+ else if (!correctResult.success) {
console.error('正解SQLでエラーが発生:', correctResult.error);
- return {
- correct: false,
- message: "チャレンジデータに問題があります。管理者に報告してください。"
- };
+ errorType = 'system';
+ message = "チャレンジデータに問題があります。管理者に報告してください。";
}
-
// 結果を比較
- const isCorrect = this.compareQueryResults(userResult, correctResult);
+ else {
+ isCorrect = this.compareQueryResults(userResult, correctResult);
+
+ if (isCorrect) {
+ message = "正解です!素晴らしい!";
+ } else {
+ errorType = 'logic';
+ message = "結果が正解と一致しません。もう一度確認してください。";
+ }
+ }
+
+ // パフォーマンスデータを記録
+ const challengeData = {
+ correct: isCorrect,
+ score: isCorrect ? this.calculateScore() : 0,
+ attempts: this.attempts,
+ hintsUsed: this.hintsUsed,
+ timeSpent: Date.now() - this.startTime,
+ challenge: challenge,
+ errorType: errorType
+ };
+ this.trackPerformanceData(challengeData);
+
+ // 正解の場合のみ進捗を更新
if (isCorrect) {
- this.calculateScore();
- return {
- correct: true,
- message: "正解です!素晴らしい!",
- score: this.score,
- userResult: userResult,
- correctResult: correctResult
- };
- } else {
- return {
- correct: false,
- message: "結果が正解と一致しません。もう一度確認してください。",
- userResult: userResult,
- correctResult: correctResult
- };
+ this.onChallengeCompleted();
}
+ const result = {
+ correct: isCorrect,
+ message: message,
+ score: isCorrect ? challengeData.score : 0
+ };
+
+ // 結果データを含める(デバッグ用)
+ if (userResult && correctResult) {
+ result.userResult = userResult;
+ result.correctResult = correctResult;
+ }
+
+ return result;
+
} catch (error) {
console.error('SQL実行エラー:', error);
+
+ const challengeData = {
+ correct: false,
+ score: 0,
+ attempts: this.attempts,
+ hintsUsed: this.hintsUsed,
+ timeSpent: Date.now() - this.startTime,
+ challenge: challenge,
+ errorType: 'system'
+ };
+ this.trackPerformanceData(challengeData);
+
return {
correct: false,
message: `実行エラー: ${error.message}`
@@ -270,6 +513,21 @@ export class GameEngine {
const challengeScore = Math.max(10, baseScore - attemptPenalty - hintPenalty + timeBonus);
this.score += challengeScore;
+ return challengeScore;
+ }
+
+ /**
+ * パフォーマンスデータを追跡
+ */
+ trackPerformanceData(challengeData) {
+ if (this.adaptiveLearning && this.adaptiveLearning.isInitialized() &&
+ this.currentCourse && challengeData.challenge && challengeData.challenge.lessonId) {
+ this.adaptiveLearning.trackPerformance(
+ this.currentCourse.id,
+ challengeData.challenge.lessonId,
+ challengeData
+ );
+ }
}
getHint(level = 0) {
@@ -296,4 +554,540 @@ export class GameEngine {
this.attempts = 0;
this.startTime = Date.now();
}
+
+ /**
+ * CourseManagerを設定
+ */
+ setCourseManager(courseManager) {
+ this.courseManager = courseManager;
+ }
+
+ /**
+ * AdaptiveLearningを設定
+ */
+ setAdaptiveLearning(adaptiveLearning) {
+ this.adaptiveLearning = adaptiveLearning;
+ }
+
+ /**
+ * 現在のコースを設定
+ */
+ async setCourse(course) {
+ this.currentCourse = course;
+
+ // コースに基づいてチャレンジを読み込み
+ await this.loadCourseChallenge();
+
+ // 進捗に基づいて現在のチャレンジインデックスを設定
+ this.setCurrentChallengeFromProgress();
+ }
+
+ /**
+ * 現在のコースのチャレンジを読み込み
+ */
+ async loadCourseChallenge() {
+ if (!this.currentCourse || !this.courseManager) {
+ console.warn('コースまたはCourseManagerが設定されていません');
+ return;
+ }
+
+ try {
+ // コースの全レッスンIDを取得
+ const allLessons = [];
+ this.currentCourse.modules.forEach(module => {
+ module.lessons.forEach(lessonId => {
+ allLessons.push(lessonId);
+ });
+ });
+
+ // 各レッスンのチャレンジデータを取得
+ this.challenges = [];
+ for (const lessonId of allLessons) {
+ const challenge = this.courseManager.getCurrentCourseChallenge(lessonId);
+ if (challenge) {
+ this.challenges.push({
+ ...challenge,
+ lessonId: lessonId
+ });
+ } else {
+ console.warn(`チャレンジが見つかりません: ${lessonId}`);
+ }
+ }
+
+ console.log(`コース "${this.currentCourse.title}" のチャレンジを読み込みました: ${this.challenges.length}個`);
+
+ } catch (error) {
+ console.error('コースチャレンジ読み込みエラー:', error);
+ // フォールバック: デフォルトチャレンジを読み込み
+ await this.loadChallenges();
+ }
+ }
+
+ /**
+ * 進捗に基づいて現在のチャレンジインデックスを設定
+ */
+ setCurrentChallengeFromProgress() {
+ if (!this.currentCourse || !this.courseManager) {
+ return;
+ }
+
+ const progress = this.courseManager.getCurrentCourseProgress();
+ if (!progress) {
+ this.currentChallengeIndex = 0;
+ return;
+ }
+
+ // 完了していない最初のレッスンを見つける
+ for (let i = 0; i < this.challenges.length; i++) {
+ const challenge = this.challenges[i];
+ if (challenge.lessonId && !progress.completedLessons.includes(challenge.lessonId)) {
+ // レッスンがアンロックされているかチェック
+ if (this.courseManager.isLessonUnlocked(this.currentCourse.id, challenge.lessonId)) {
+ this.currentChallengeIndex = i;
+ return;
+ }
+ }
+ }
+
+ // 全て完了している場合は最後のチャレンジ
+ this.currentChallengeIndex = Math.max(0, this.challenges.length - 1);
+ }
+
+ /**
+ * 現在のコースの進捗を取得
+ */
+ getCurrentCourseProgress() {
+ if (!this.currentCourse || !this.courseManager) {
+ return this.getProgress();
+ }
+
+ const progress = this.courseManager.getCurrentCourseProgress();
+ if (!progress) {
+ return this.getProgress();
+ }
+
+ const totalLessons = this.challenges.length;
+ const completedLessons = progress.completedLessons.length;
+
+ return {
+ current: completedLessons + 1,
+ total: totalLessons,
+ percentage: totalLessons > 0 ? (completedLessons / totalLessons) * 100 : 0,
+ courseProgress: progress,
+ isCompleted: progress.isCompleted,
+ totalScore: progress.totalScore || 0,
+ completedModules: progress.completedModules.length,
+ totalModules: this.currentCourse.modules.length
+ };
+ }
+
+ /**
+ * コースに基づいてフィルタされたチャレンジを取得
+ */
+ getFilteredChallenges() {
+ if (!this.currentCourse) {
+ return this.challenges;
+ }
+
+ // 現在のコースのレッスンのみを返す
+ return this.challenges.filter(challenge => {
+ if (!challenge.lessonId) return true;
+
+ // レッスンがコースに含まれているかチェック
+ for (const module of this.currentCourse.modules) {
+ if (module.lessons.includes(challenge.lessonId)) {
+ return true;
+ }
+ }
+ return false;
+ });
+ }
+
+ /**
+ * チャレンジ完了時の処理(コース対応)
+ */
+ onChallengeCompleted() {
+ const challenge = this.getCurrentChallenge();
+
+ if (this.currentCourse && this.courseManager && challenge.lessonId) {
+ // コースの進捗を更新
+ this.courseManager.updateProgress(this.currentCourse.id, challenge.lessonId, this.score);
+ console.log(`レッスン完了: ${challenge.lessonId}`);
+
+ // 適応的学習システムでパフォーマンスを追跡
+ if (this.adaptiveLearning && this.adaptiveLearning.isInitialized()) {
+ const challengeData = {
+ correct: true,
+ score: this.score,
+ attempts: this.attempts,
+ hintsUsed: this.hintsUsed,
+ timeSpent: Date.now() - this.startTime,
+ challenge: challenge
+ };
+ this.adaptiveLearning.trackPerformance(this.currentCourse.id, challenge.lessonId, challengeData);
+ }
+
+ // ProgressUIに進捗更新を通知
+ if (window.progressUI && typeof window.progressUI.onProgressUpdated === 'function') {
+ window.progressUI.onProgressUpdated();
+ }
+ }
+ }
+
+
+
+ /**
+ * 指定されたレッスンに現在のチャレンジを設定(アクセス制御付き)
+ */
+ setCurrentLesson(lessonId) {
+ // コースが設定されている場合はアクセス制御をチェック
+ if (this.currentCourse && this.courseManager) {
+ const accessResult = this.courseManager.attemptLessonAccess(this.currentCourse.id, lessonId);
+ if (!accessResult.success) {
+ console.warn(`レッスンアクセス拒否: ${accessResult.error}`);
+ console.log(`推奨アクション: ${accessResult.suggestedAction}`);
+ return {
+ success: false,
+ error: accessResult.error,
+ suggestedAction: accessResult.suggestedAction
+ };
+ }
+ }
+
+ // レッスンを検索して設定
+ for (let i = 0; i < this.challenges.length; i++) {
+ const challenge = this.challenges[i];
+ if (challenge.lessonId === lessonId) {
+ this.currentChallengeIndex = i;
+ this.attempts = 0;
+ this.hintsUsed = 0;
+ this.startTime = Date.now();
+ console.log(`レッスンに移動: ${lessonId}`);
+ return {
+ success: true,
+ lessonId: lessonId,
+ challengeIndex: i
+ };
+ }
+ }
+
+ return {
+ success: false,
+ error: `レッスン ${lessonId} が見つかりません`,
+ suggestedAction: 'コースデータを確認してください'
+ };
+ }
+
+ /**
+ * レッスンアクセス試行(UI用)
+ */
+ attemptLessonAccess(lessonId) {
+ if (!this.currentCourse || !this.courseManager) {
+ return this.setCurrentLesson(lessonId);
+ }
+
+ const accessResult = this.courseManager.attemptLessonAccess(this.currentCourse.id, lessonId);
+
+ if (accessResult.success) {
+ return this.setCurrentLesson(lessonId);
+ } else {
+ // ErrorHandlerを使用して不正アクセスを処理
+ if (window.errorHandler) {
+ const errorResult = window.errorHandler.handleError('INVALID_ACCESS', new Error(accessResult.error), {
+ courseId: this.currentCourse.id,
+ lessonId: lessonId,
+ currentProgress: this.courseManager.getCurrentCourseProgress(),
+ attemptedAction: 'lesson_access',
+ lessonLocked: !this.courseManager.isLessonUnlocked(this.currentCourse.id, lessonId)
+ });
+
+ return {
+ success: false,
+ error: accessResult.error,
+ suggestedAction: accessResult.suggestedAction,
+ lessonId: lessonId,
+ errorHandled: true,
+ errorResult: errorResult
+ };
+ }
+
+ return {
+ success: false,
+ error: accessResult.error,
+ suggestedAction: accessResult.suggestedAction,
+ lessonId: lessonId
+ };
+ }
+ }
+
+ /**
+ * コース完了判定
+ */
+ isCourseCompleted() {
+ if (!this.currentCourse || !this.courseManager) {
+ return false;
+ }
+
+ const progress = this.courseManager.getCurrentCourseProgress();
+ return progress ? progress.isCompleted : false;
+ }
+
+ /**
+ * モジュール完了判定
+ */
+ isModuleCompleted(moduleId) {
+ if (!this.currentCourse || !this.courseManager) {
+ return false;
+ }
+
+ const progress = this.courseManager.getCurrentCourseProgress();
+ return progress ? progress.completedModules.includes(moduleId) : false;
+ }
+
+ /**
+ * 現在のモジュールを取得
+ */
+ getCurrentModule() {
+ if (!this.currentCourse) {
+ return null;
+ }
+
+ const challenge = this.getCurrentChallenge();
+ if (!challenge || !challenge.lessonId) {
+ return null;
+ }
+
+ // レッスンが属するモジュールを見つける
+ for (const module of this.currentCourse.modules) {
+ if (module.lessons.includes(challenge.lessonId)) {
+ return module;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * コース全体のスコア計算
+ */
+ calculateCourseScore() {
+ if (!this.currentCourse || !this.courseManager) {
+ return 0;
+ }
+
+ const progress = this.courseManager.getCurrentCourseProgress();
+ if (!progress) {
+ return 0;
+ }
+
+ // 基本スコア(完了したレッスン数に基づく)
+ const completedLessons = progress.completedLessons.length;
+ const totalLessons = this.challenges.length;
+ const completionRatio = totalLessons > 0 ? completedLessons / totalLessons : 0;
+
+ // 完了ボーナス
+ const completionBonus = progress.isCompleted ? 500 : 0;
+
+ // モジュール完了ボーナス
+ const moduleBonus = progress.completedModules.length * 100;
+
+ // 時間ボーナス(早期完了)
+ let timeBonus = 0;
+ if (progress.startDate && progress.lastAccessed) {
+ const startTime = new Date(progress.startDate).getTime();
+ const endTime = new Date(progress.lastAccessed).getTime();
+ const hoursSpent = (endTime - startTime) / (1000 * 60 * 60);
+ const estimatedHours = this.currentCourse.estimatedHours || 8;
+
+ if (hoursSpent < estimatedHours) {
+ timeBonus = Math.max(0, (estimatedHours - hoursSpent) * 10);
+ }
+ }
+
+ return Math.floor((progress.totalScore || 0) + completionBonus + moduleBonus + timeBonus);
+ }
+
+ /**
+ * 次に利用可能なチャレンジを取得
+ */
+ getNextAvailableChallenge() {
+ if (!this.currentCourse || !this.courseManager) {
+ return null;
+ }
+
+ for (let i = this.currentChallengeIndex + 1; i < this.challenges.length; i++) {
+ const challenge = this.challenges[i];
+ if (challenge.lessonId && this.courseManager.isLessonUnlocked(this.currentCourse.id, challenge.lessonId)) {
+ return {
+ index: i,
+ challenge: challenge
+ };
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * コース進捗の詳細情報を取得
+ */
+ getCourseProgressDetails() {
+ if (!this.currentCourse || !this.courseManager) {
+ return null;
+ }
+
+ const progress = this.courseManager.getCurrentCourseProgress();
+ if (!progress) {
+ return null;
+ }
+
+ const moduleProgress = this.currentCourse.modules.map(module => {
+ const completedLessonsInModule = module.lessons.filter(lessonId =>
+ progress.completedLessons.includes(lessonId)
+ ).length;
+
+ return {
+ id: module.id,
+ title: module.title,
+ totalLessons: module.lessons.length,
+ completedLessons: completedLessonsInModule,
+ isCompleted: progress.completedModules.includes(module.id),
+ percentage: module.lessons.length > 0 ? (completedLessonsInModule / module.lessons.length) * 100 : 0
+ };
+ });
+
+ return {
+ courseId: this.currentCourse.id,
+ courseTitle: this.currentCourse.title,
+ totalScore: this.calculateCourseScore(),
+ isCompleted: progress.isCompleted,
+ totalLessons: this.challenges.length,
+ completedLessons: progress.completedLessons.length,
+ totalModules: this.currentCourse.modules.length,
+ completedModules: progress.completedModules.length,
+ moduleProgress: moduleProgress,
+ startDate: progress.startDate,
+ lastAccessed: progress.lastAccessed
+ };
+ }
+
+ /**
+ * 学習統計を取得
+ */
+ getLearningStats() {
+ if (!this.currentCourse || !this.courseManager) {
+ return null;
+ }
+
+ const progressDetails = this.getCourseProgressDetails();
+ if (!progressDetails) {
+ return null;
+ }
+
+ const averageScore = progressDetails.completedLessons > 0 ?
+ progressDetails.totalScore / progressDetails.completedLessons : 0;
+
+ let studyTime = 0;
+ if (progressDetails.startDate && progressDetails.lastAccessed) {
+ const startTime = new Date(progressDetails.startDate).getTime();
+ const endTime = new Date(progressDetails.lastAccessed).getTime();
+ studyTime = (endTime - startTime) / (1000 * 60 * 60); // 時間単位
+ }
+
+ return {
+ totalScore: progressDetails.totalScore,
+ averageScore: Math.round(averageScore),
+ studyTimeHours: Math.round(studyTime * 10) / 10,
+ completionRate: progressDetails.completedLessons / progressDetails.totalLessons,
+ moduleCompletionRate: progressDetails.completedModules / progressDetails.totalModules,
+ estimatedTimeRemaining: this.calculateEstimatedTimeRemaining()
+ };
+ }
+
+ /**
+ * 残り推定時間を計算
+ */
+ calculateEstimatedTimeRemaining() {
+ if (!this.currentCourse || !this.courseManager) {
+ return 0;
+ }
+
+ const progress = this.courseManager.getCurrentCourseProgress();
+ if (!progress) {
+ return this.currentCourse.estimatedHours || 0;
+ }
+
+ const totalLessons = this.challenges.length;
+ const completedLessons = progress.completedLessons.length;
+ const remainingLessons = totalLessons - completedLessons;
+
+ if (remainingLessons <= 0) {
+ return 0;
+ }
+
+ const estimatedHours = this.currentCourse.estimatedHours || 8;
+ const remainingRatio = remainingLessons / totalLessons;
+
+ return Math.round(estimatedHours * remainingRatio * 10) / 10;
+ }
+
+ /**
+ * 適応的学習の推奨事項を取得
+ */
+ getAdaptiveLearningRecommendations() {
+ if (!this.adaptiveLearning || !this.adaptiveLearning.isInitialized() || !this.currentCourse) {
+ return null;
+ }
+
+ const challenge = this.getCurrentChallenge();
+ if (!challenge || !challenge.lessonId) {
+ return null;
+ }
+
+ return this.adaptiveLearning.recommendNextLesson(this.currentCourse.id, challenge.lessonId);
+ }
+
+ /**
+ * 困難な概念を取得
+ */
+ getDifficultConcepts() {
+ if (!this.adaptiveLearning || !this.adaptiveLearning.isInitialized() || !this.currentCourse) {
+ return [];
+ }
+
+ return this.adaptiveLearning.detectDifficultConcepts(this.currentCourse.id);
+ }
+
+ /**
+ * 追加練習問題を取得
+ */
+ getAdditionalPractice(conceptId) {
+ if (!this.adaptiveLearning || !this.adaptiveLearning.isInitialized() || !this.currentCourse) {
+ return null;
+ }
+
+ return this.adaptiveLearning.suggestAdditionalPractice(this.currentCourse.id, conceptId);
+ }
+
+ /**
+ * 学習レポートを生成
+ */
+ generateLearningReport() {
+ if (!this.adaptiveLearning || !this.adaptiveLearning.isInitialized() || !this.currentCourse) {
+ return null;
+ }
+
+ return this.adaptiveLearning.generateLearningReport(this.currentCourse.id);
+ }
+
+ /**
+ * ユーザーの習熟度を分析
+ */
+ analyzeUserProficiency() {
+ if (!this.adaptiveLearning || !this.adaptiveLearning.isInitialized() || !this.currentCourse) {
+ return null;
+ }
+
+ return this.adaptiveLearning.analyzeUserProficiency(this.currentCourse.id);
+ }
}
\ No newline at end of file
diff --git a/js/main.js b/js/main.js
index 52f625c..a6c66c2 100644
--- a/js/main.js
+++ b/js/main.js
@@ -2,16 +2,42 @@ import { DuckDBManager } from './duckdb-manager.js';
import { GameEngine } from './game-engine.js';
import { UIController } from './ui-controller.js';
import { SQLAutoComplete } from './sql-autocomplete.js';
+import { CourseUI } from './course-ui.js';
+import { CourseManager } from './course-manager.js';
+import { ProgressManager } from './progress-manager.js';
+import { ProgressUI } from './progress-ui.js';
+import { ErrorHandler } from './error-handler.js';
+import { NotificationSystem } from './notification-system.js';
+import { AdaptiveLearning } from './adaptive-learning.js';
+import { AdaptiveLearningUI } from './adaptive-learning-ui.js';
// グローバル変数
let dbManager;
let gameEngine;
let uiController;
+let courseManager;
+let courseUI;
+let progressUI;
+let adaptiveLearning;
+let adaptiveLearningUI;
// アプリケーション初期化
async function initializeApp() {
try {
+ // エラーハンドリングシステムを最初に初期化
+ const errorHandler = new ErrorHandler();
+ const notificationSystem = new NotificationSystem();
+
+ // エラーハンドラーに通知システムを登録
+ errorHandler.addNotificationCallback((message, type) => {
+ notificationSystem.showErrorNotification(message, type);
+ });
+
+ // グローバルアクセス用
+ window.errorHandler = errorHandler;
+ window.notificationSystem = notificationSystem;
+
// DuckDB初期化
dbManager = new DuckDBManager();
const dbInitialized = await dbManager.initialize();
@@ -20,21 +46,50 @@ async function initializeApp() {
throw new Error('データベースの初期化に失敗しました');
}
+ // ProgressManager初期化
+ const progressManager = new ProgressManager();
+
+ // CourseManager初期化
+ courseManager = new CourseManager();
+ courseManager.setProgressManager(progressManager);
+ await courseManager.loadCourses();
+
// ゲームエンジン初期化
gameEngine = new GameEngine();
+ gameEngine.setCourseManager(courseManager);
+
+ // 適応的学習システム初期化
+ adaptiveLearning = new AdaptiveLearning(courseManager, gameEngine);
+ await adaptiveLearning.initialize();
+ gameEngine.setAdaptiveLearning(adaptiveLearning);
+
+ // 既存のチャレンジ読み込み(フォールバック用)
await gameEngine.loadChallenges();
- // グローバルアクセス用(スキーマ読み込み前に設定)
+ // グローバルアクセス用
window.dbManager = dbManager;
window.gameEngine = gameEngine;
+ window.courseManager = courseManager;
+ window.adaptiveLearning = adaptiveLearning;
// UIコントローラー初期化
const autoComplete = new SQLAutoComplete();
uiController = new UIController(gameEngine, autoComplete);
+ uiController.setCourseManager(courseManager);
+ uiController.initializeCourseUI();
window.uiController = uiController;
+
+ // CourseUI初期化
+ courseUI = new CourseUI(courseManager, gameEngine);
+ window.courseUI = courseUI;
- // 最初のチャレンジを表示
- uiController.updateChallenge();
+ // ProgressUI初期化
+ progressUI = new ProgressUI(courseManager, gameEngine);
+ window.progressUI = progressUI;
+
+ // AdaptiveLearningUI初期化
+ adaptiveLearningUI = new AdaptiveLearningUI(adaptiveLearning, gameEngine, courseManager);
+ window.adaptiveLearningUI = adaptiveLearningUI;
// スキーマ情報を読み込み
await uiController.loadSchemaInfo();
@@ -42,6 +97,9 @@ async function initializeApp() {
// 実行ボタンを有効化
document.getElementById('run-query').disabled = false;
+ // コースシステムの初期化
+ await initializeCourseSystem();
+
// ローディング画面を非表示
uiController.hideLoading();
@@ -49,6 +107,33 @@ async function initializeApp() {
} catch (error) {
console.error('初期化エラー:', error);
+
+ // ErrorHandlerが利用可能な場合は使用
+ if (window.errorHandler) {
+ window.errorHandler.logError('INITIALIZATION_ERROR', error, {
+ operation: 'main_initialization',
+ timestamp: new Date().toISOString()
+ });
+ }
+
+ // NotificationSystemが利用可能な場合は通知を表示
+ if (window.notificationSystem) {
+ window.notificationSystem.show(
+ 'アプリケーションの初期化中にエラーが発生しました。ページを再読み込みしてください。',
+ 'error',
+ {
+ duration: 0,
+ actions: [
+ {
+ label: '再読み込み',
+ callback: () => location.reload(),
+ primary: true
+ }
+ ]
+ }
+ );
+ }
+
document.getElementById('loading-screen').innerHTML = `
エラーが発生しました
@@ -59,6 +144,380 @@ async function initializeApp() { } } +// コースシステムの初期化 +async function initializeCourseSystem() { + try { + console.log('コースシステムの初期化を開始します...'); + + // CourseManagerの初期化状態を確認 + if (!courseManager.initialized) { + console.warn('CourseManagerが初期化されていません。再初期化を試行します。'); + await courseManager.loadCourses(); + } + + // 既存ユーザーの進捗復元処理 + const restoredCourse = await restoreUserProgress(); + + if (restoredCourse) { + // 既存のコースがある場合はそのコースを設定 + await gameEngine.setCourse(restoredCourse); + + // 進捗に基づいて適切なレッスンに移動 + const nextLesson = courseManager.getNextLesson(restoredCourse.id); + if (nextLesson) { + console.log(`次のレッスンに移動: ${nextLesson.lessonId} (モジュール: ${nextLesson.moduleTitle})`); + // 適切なレッスンを設定 + await gameEngine.setCurrentLesson(nextLesson.lessonId); + } + + // UI更新 + uiController.updateChallenge(); + progressUI.updateProgressDisplay(); + + // 復元完了の通知 + if (window.notificationSystem) { + window.notificationSystem.show( + `学習を再開しました: ${restoredCourse.title}`, + 'success', + { duration: 3000 } + ); + } + + console.log(`既存のコースを復元しました: ${restoredCourse.title}`); + } else { + // 初回アクセス時のコース選択画面表示 + await showInitialCourseSelection(); + } + + console.log('コースシステムの初期化が完了しました'); + + } catch (error) { + console.error('コースシステム初期化エラー:', error); + + // ErrorHandlerを使用してエラーを処理 + if (window.errorHandler) { + window.errorHandler.logError('COURSE_SYSTEM_INIT_ERROR', error, { + operation: 'initializeCourseSystem', + timestamp: new Date().toISOString() + }); + } + + // エラー時のフォールバック処理 + await handleCourseSystemInitError(error); + } +} + +// 既存ユーザーの進捗復元処理 +async function restoreUserProgress() { + try { + console.log('既存ユーザーの進捗復元を開始します...'); + + // 現在選択されているコースを取得 + const currentCourse = courseManager.getCurrentCourse(); + + if (currentCourse) { + // コースの進捗データを取得 + const progress = courseManager.getCourseProgress(currentCourse.id); + + if (progress) { + console.log(`進捗データを発見: ${currentCourse.title}`, { + completedLessons: progress.completedLessons.length, + completedModules: progress.completedModules.length, + totalScore: progress.totalScore, + lastAccessed: progress.lastAccessed + }); + + // 最終アクセス日時を更新 + courseManager.progressManager.updateLastAccessed(currentCourse.id); + + // 進捗の整合性をチェック + const validationResult = validateProgressIntegrity(currentCourse, progress); + if (!validationResult.isValid) { + console.warn('進捗データの整合性に問題があります:', validationResult.issues); + + // 自動修復を試行 + const repairResult = await repairProgressData(currentCourse.id, progress, validationResult.issues); + if (repairResult.success) { + console.log('進捗データの自動修復が完了しました'); + } else { + console.error('進捗データの修復に失敗しました:', repairResult.error); + } + } + + return currentCourse; + } else { + console.log(`コース ${currentCourse.title} の進捗データが見つかりません。新規として扱います。`); + // 進捗データを初期化 + courseManager.initializeCourseProgress(currentCourse.id); + return currentCourse; + } + } + + console.log('選択されたコースが見つかりません'); + return null; + + } catch (error) { + console.error('進捗復元エラー:', error); + + // ErrorHandlerを使用してエラーを処理 + if (window.errorHandler) { + window.errorHandler.logError('PROGRESS_RESTORE_ERROR', error, { + operation: 'restoreUserProgress', + timestamp: new Date().toISOString() + }); + } + + return null; + } +} + +// 初回アクセス時のコース選択画面表示 +async function showInitialCourseSelection() { + try { + console.log('初回アクセス: コース選択画面を表示します'); + + // 利用可能なコース一覧を取得 + const availableCourses = courseManager.getCourses(); + + if (availableCourses.length === 0) { + throw new Error('利用可能なコースが見つかりません'); + } + + console.log(`${availableCourses.length}個のコースが利用可能です:`, + availableCourses.map(course => course.title)); + + // コース選択画面を表示 + courseUI.showCourseSelection(); + + // ウェルカムメッセージを表示 + if (window.notificationSystem) { + window.notificationSystem.show( + 'SQL学習プラットフォームへようこそ!学習したいコースを選択してください。', + 'info', + { + duration: 5000, + position: 'top-center' + } + ); + } + + } catch (error) { + console.error('初回コース選択画面表示エラー:', error); + throw error; + } +} + +// 進捗データの整合性チェック +function validateProgressIntegrity(course, progress) { + const issues = []; + let isValid = true; + + try { + // 完了済みレッスンの妥当性チェック + const allValidLessons = course.modules.flatMap(module => module.lessons); + const invalidLessons = progress.completedLessons.filter(lessonId => + !allValidLessons.includes(lessonId) + ); + + if (invalidLessons.length > 0) { + issues.push({ + type: 'invalid_lessons', + description: '存在しないレッスンが完了済みとしてマークされています', + data: invalidLessons + }); + isValid = false; + } + + // 完了済みモジュールの妥当性チェック + const allValidModules = course.modules.map(module => module.id); + const invalidModules = progress.completedModules.filter(moduleId => + !allValidModules.includes(moduleId) + ); + + if (invalidModules.length > 0) { + issues.push({ + type: 'invalid_modules', + description: '存在しないモジュールが完了済みとしてマークされています', + data: invalidModules + }); + isValid = false; + } + + // モジュール完了の論理的整合性チェック + for (const moduleId of progress.completedModules) { + const module = course.modules.find(m => m.id === moduleId); + if (module) { + const moduleAllLessonsCompleted = module.lessons.every(lessonId => + progress.completedLessons.includes(lessonId) + ); + + if (!moduleAllLessonsCompleted) { + issues.push({ + type: 'module_lesson_mismatch', + description: `モジュール ${moduleId} が完了済みですが、すべてのレッスンが完了していません`, + data: { moduleId, incompleteLessons: module.lessons.filter(lessonId => + !progress.completedLessons.includes(lessonId) + )} + }); + isValid = false; + } + } + } + + // 日付の妥当性チェック + if (progress.startDate && progress.lastAccessed) { + const startDate = new Date(progress.startDate); + const lastAccessed = new Date(progress.lastAccessed); + + if (lastAccessed < startDate) { + issues.push({ + type: 'invalid_dates', + description: '最終アクセス日時が開始日時より前になっています', + data: { startDate: progress.startDate, lastAccessed: progress.lastAccessed } + }); + isValid = false; + } + } + + } catch (error) { + console.error('進捗整合性チェックエラー:', error); + issues.push({ + type: 'validation_error', + description: '整合性チェック中にエラーが発生しました', + data: error.message + }); + isValid = false; + } + + return { isValid, issues }; +} + +// 進捗データの自動修復 +async function repairProgressData(courseId, progress, issues) { + try { + console.log(`進捗データの自動修復を開始: ${courseId}`); + + let repaired = false; + + for (const issue of issues) { + switch (issue.type) { + case 'invalid_lessons': + // 存在しないレッスンを削除 + const validLessons = progress.completedLessons.filter(lessonId => + !issue.data.includes(lessonId) + ); + courseManager.progressManager.setCompletedLessons(courseId, validLessons); + repaired = true; + console.log(`無効なレッスンを削除しました:`, issue.data); + break; + + case 'invalid_modules': + // 存在しないモジュールを削除 + const validModules = progress.completedModules.filter(moduleId => + !issue.data.includes(moduleId) + ); + courseManager.progressManager.setCompletedModules(courseId, validModules); + repaired = true; + console.log(`無効なモジュールを削除しました:`, issue.data); + break; + + case 'module_lesson_mismatch': + // モジュール完了状態をリセット + courseManager.progressManager.removeCompletedModule(courseId, issue.data.moduleId); + repaired = true; + console.log(`モジュール完了状態をリセットしました: ${issue.data.moduleId}`); + break; + + case 'invalid_dates': + // 最終アクセス日時を現在時刻に更新 + courseManager.progressManager.updateLastAccessed(courseId); + repaired = true; + console.log('最終アクセス日時を修正しました'); + break; + } + } + + if (repaired) { + console.log('進捗データの自動修復が完了しました'); + return { success: true }; + } else { + return { success: false, error: '修復可能な問題が見つかりませんでした' }; + } + + } catch (error) { + console.error('進捗データ修復エラー:', error); + return { success: false, error: error.message }; + } +} + +// コースシステム初期化エラーのハンドリング +async function handleCourseSystemInitError(error) { + try { + console.log('コースシステム初期化エラーのフォールバック処理を実行します'); + + // エラーの種類に応じた処理 + if (error.message.includes('courses.json') || error.message.includes('COURSE_LOAD_ERROR')) { + // コースデータ読み込みエラーの場合 + console.log('デフォルトコースでの初期化を試行します'); + + // デフォルトコースを読み込み + await courseManager.loadDefaultCourse(); + + // デフォルトコースを設定 + const defaultCourse = courseManager.getCourses()[0]; + if (defaultCourse) { + courseManager.selectCourse(defaultCourse.id); + await gameEngine.setCourse(defaultCourse); + uiController.updateChallenge(); + + // ユーザーに通知 + if (window.notificationSystem) { + window.notificationSystem.show( + 'コースデータの読み込みに問題がありましたが、デフォルトコースで学習を開始できます。', + 'warning', + { duration: 5000 } + ); + } + + console.log('デフォルトコースでの初期化が完了しました'); + return; + } + } + + // その他のエラーの場合は基本的なUI更新のみ実行 + console.log('基本的なUI更新を実行します'); + uiController.updateChallenge(); + + // ユーザーにエラーを通知 + if (window.notificationSystem) { + window.notificationSystem.show( + 'コースシステムの初期化中に問題が発生しました。一部の機能が制限される可能性があります。', + 'error', + { + duration: 0, + actions: [ + { + label: 'ページを再読み込み', + callback: () => location.reload(), + primary: true + } + ] + } + ); + } + + } catch (fallbackError) { + console.error('フォールバック処理でもエラーが発生しました:', fallbackError); + + // 最後の手段として基本的なUI更新 + try { + uiController.updateChallenge(); + } catch (finalError) { + console.error('最終的なUI更新も失敗しました:', finalError); + } + } +} + // ページ読み込み完了時に初期化 document.addEventListener('DOMContentLoaded', initializeApp); diff --git a/js/notification-system.js b/js/notification-system.js new file mode 100644 index 0000000..04bdd9f --- /dev/null +++ b/js/notification-system.js @@ -0,0 +1,580 @@ +/** + * NotificationSystem - ユーザー通知システム + * エラーハンドラーと連携してユーザーに適切な通知を表示 + */ +class NotificationSystem { + constructor() { + this.notifications = []; + this.maxNotifications = 5; + this.defaultDuration = 5000; // 5秒 + this.container = null; + + this.initializeContainer(); + this.setupStyles(); + } + + /** + * 通知コンテナを初期化 + */ + initializeContainer() { + // 既存のコンテナをチェック + this.container = document.getElementById('notification-container'); + + if (!this.container) { + this.container = document.createElement('div'); + this.container.id = 'notification-container'; + this.container.className = 'notification-container'; + document.body.appendChild(this.container); + } + } + + /** + * 通知システムのスタイルを設定 + */ + setupStyles() { + const styleId = 'notification-system-styles'; + + // 既存のスタイルをチェック + if (document.getElementById(styleId)) { + return; + } + + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + .notification-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 10000; + max-width: 400px; + pointer-events: none; + } + + .notification { + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + margin-bottom: 10px; + padding: 16px; + border-left: 4px solid #007bff; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + pointer-events: auto; + position: relative; + max-width: 100%; + word-wrap: break-word; + } + + .notification.show { + opacity: 1; + transform: translateX(0); + } + + .notification.hide { + opacity: 0; + transform: translateX(100%); + } + + .notification.info { + border-left-color: #007bff; + } + + .notification.success { + border-left-color: #28a745; + } + + .notification.warning { + border-left-color: #ffc107; + } + + .notification.error { + border-left-color: #dc3545; + } + + .notification-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 8px; + } + + .notification-title { + font-weight: 600; + font-size: 14px; + margin: 0; + color: #333; + } + + .notification-close { + background: none; + border: none; + font-size: 18px; + cursor: pointer; + color: #666; + padding: 0; + margin-left: 10px; + line-height: 1; + } + + .notification-close:hover { + color: #333; + } + + .notification-message { + font-size: 13px; + color: #666; + line-height: 1.4; + margin: 0; + } + + .notification-actions { + margin-top: 12px; + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .notification-action { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s; + } + + .notification-action:hover { + background: #e9ecef; + } + + .notification-action.primary { + background: #007bff; + color: white; + border-color: #007bff; + } + + .notification-action.primary:hover { + background: #0056b3; + } + + .notification-progress { + position: absolute; + bottom: 0; + left: 0; + height: 2px; + background: rgba(0, 123, 255, 0.3); + transition: width linear; + } + + .notification.info .notification-progress { + background: rgba(0, 123, 255, 0.3); + } + + .notification.success .notification-progress { + background: rgba(40, 167, 69, 0.3); + } + + .notification.warning .notification-progress { + background: rgba(255, 193, 7, 0.3); + } + + .notification.error .notification-progress { + background: rgba(220, 53, 69, 0.3); + } + + @media (max-width: 480px) { + .notification-container { + top: 10px; + right: 10px; + left: 10px; + max-width: none; + } + + .notification { + margin-bottom: 8px; + padding: 12px; + } + } + `; + + document.head.appendChild(style); + } + + /** + * 通知を表示 + * @param {string} message - 通知メッセージ + * @param {string} type - 通知タイプ (info, success, warning, error) + * @param {Object} options - 追加オプション + */ + show(message, type = 'info', options = {}) { + const notification = this.createNotification(message, type, options); + + // 最大通知数を超える場合は古い通知を削除 + if (this.notifications.length >= this.maxNotifications) { + const oldestNotification = this.notifications.shift(); + this.removeNotification(oldestNotification); + } + + this.notifications.push(notification); + this.container.appendChild(notification.element); + + // アニメーション用の遅延 + setTimeout(() => { + notification.element.classList.add('show'); + }, 10); + + // 自動削除の設定 + if (options.duration !== 0) { + const duration = options.duration || this.defaultDuration; + notification.autoRemoveTimer = setTimeout(() => { + this.removeNotification(notification); + }, duration); + + // プログレスバーのアニメーション + if (notification.progressBar) { + notification.progressBar.style.width = '100%'; + setTimeout(() => { + notification.progressBar.style.width = '0%'; + notification.progressBar.style.transition = `width ${duration}ms linear`; + }, 50); + } + } + + return notification; + } + + /** + * 通知要素を作成 + * @param {string} message - メッセージ + * @param {string} type - タイプ + * @param {Object} options - オプション + * @returns {Object} 通知オブジェクト + */ + createNotification(message, type, options) { + const id = `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const element = document.createElement('div'); + element.className = `notification ${type}`; + element.id = id; + + const title = this.getTypeTitle(type); + const actions = options.actions || []; + const showProgress = options.duration !== 0; + + element.innerHTML = ` +
+
+ ${title}
+ +${message}
+ ${actions.length > 0 ? ` +
+ ${actions.map((action, index) => `
+
+ `).join('')}
+
+ ` : ''}
+ ${showProgress ? '' : ''}
+ `;
+
+ const notification = {
+ id: id,
+ element: element,
+ type: type,
+ message: message,
+ options: options,
+ progressBar: showProgress ? element.querySelector('.notification-progress') : null,
+ autoRemoveTimer: null
+ };
+
+ // イベントリスナーを設定
+ this.setupNotificationEvents(notification);
+
+ return notification;
+ }
+
+ /**
+ * 通知のイベントリスナーを設定
+ * @param {Object} notification - 通知オブジェクト
+ */
+ setupNotificationEvents(notification) {
+ const closeButton = notification.element.querySelector('.notification-close');
+ closeButton.addEventListener('click', () => {
+ this.removeNotification(notification);
+ });
+
+ // アクションボタンのイベント
+ const actionButtons = notification.element.querySelectorAll('.notification-action');
+ actionButtons.forEach((button, index) => {
+ button.addEventListener('click', () => {
+ const action = notification.options.actions[index];
+ if (action.callback && typeof action.callback === 'function') {
+ action.callback();
+ }
+
+ if (action.closeOnClick !== false) {
+ this.removeNotification(notification);
+ }
+ });
+ });
+
+ // ホバー時の自動削除停止
+ notification.element.addEventListener('mouseenter', () => {
+ if (notification.autoRemoveTimer) {
+ clearTimeout(notification.autoRemoveTimer);
+ notification.autoRemoveTimer = null;
+ }
+
+ if (notification.progressBar) {
+ notification.progressBar.style.animationPlayState = 'paused';
+ }
+ });
+
+ notification.element.addEventListener('mouseleave', () => {
+ if (notification.options.duration !== 0) {
+ const remainingTime = notification.options.duration || this.defaultDuration;
+ notification.autoRemoveTimer = setTimeout(() => {
+ this.removeNotification(notification);
+ }, remainingTime / 2); // 残り時間を短縮
+ }
+ });
+ }
+
+ /**
+ * 通知を削除
+ * @param {Object} notification - 削除する通知
+ */
+ removeNotification(notification) {
+ if (notification.autoRemoveTimer) {
+ clearTimeout(notification.autoRemoveTimer);
+ }
+
+ notification.element.classList.add('hide');
+
+ setTimeout(() => {
+ if (notification.element.parentNode) {
+ notification.element.parentNode.removeChild(notification.element);
+ }
+
+ const index = this.notifications.indexOf(notification);
+ if (index > -1) {
+ this.notifications.splice(index, 1);
+ }
+ }, 300);
+ }
+
+ /**
+ * タイプに応じたタイトルを取得
+ * @param {string} type - 通知タイプ
+ * @returns {string} タイトル
+ */
+ getTypeTitle(type) {
+ const titles = {
+ info: '情報',
+ success: '成功',
+ warning: '警告',
+ error: 'エラー'
+ };
+
+ return titles[type] || '通知';
+ }
+
+ /**
+ * 特定タイプの通知を全て削除
+ * @param {string} type - 削除する通知タイプ
+ */
+ clearByType(type) {
+ const notificationsToRemove = this.notifications.filter(n => n.type === type);
+ notificationsToRemove.forEach(notification => {
+ this.removeNotification(notification);
+ });
+ }
+
+ /**
+ * 全ての通知を削除
+ */
+ clearAll() {
+ [...this.notifications].forEach(notification => {
+ this.removeNotification(notification);
+ });
+ }
+
+ /**
+ * エラーハンドラー用の通知表示
+ * @param {string} message - メッセージ
+ * @param {string} type - タイプ
+ * @param {Object} errorContext - エラーコンテキスト
+ */
+ showErrorNotification(message, type, errorContext = {}) {
+ const actions = [];
+
+ // エラータイプに応じたアクションを追加
+ if (errorContext.suggestedActions) {
+ errorContext.suggestedActions.forEach(action => {
+ actions.push({
+ label: action.label,
+ callback: () => this.handleSuggestedAction(action),
+ primary: action.action === 'select_course' || action.action === 'complete_prerequisites'
+ });
+ });
+ }
+
+ // 共通アクション
+ if (type === 'error') {
+ actions.push({
+ label: '再読み込み',
+ callback: () => window.location.reload(),
+ primary: false
+ });
+ }
+
+ const options = {
+ duration: type === 'error' ? 0 : this.defaultDuration, // エラーは手動で閉じる
+ actions: actions
+ };
+
+ return this.show(message, type, options);
+ }
+
+ /**
+ * 推奨アクションを処理
+ * @param {Object} action - アクション情報
+ */
+ handleSuggestedAction(action) {
+ switch (action.action) {
+ case 'select_course':
+ if (window.courseUI && typeof window.courseUI.showCourseSelection === 'function') {
+ window.courseUI.showCourseSelection();
+ }
+ break;
+
+ case 'view_course_overview':
+ if (window.courseUI && typeof window.courseUI.showCourseOverview === 'function') {
+ window.courseUI.showCourseOverview();
+ }
+ break;
+
+ case 'continue_from_last':
+ if (window.gameEngine && typeof window.gameEngine.setCurrentChallengeFromProgress === 'function') {
+ window.gameEngine.setCurrentChallengeFromProgress();
+ }
+ break;
+
+ case 'complete_prerequisites':
+ if (window.progressUI && typeof window.progressUI.showProgressPanel === 'function') {
+ window.progressUI.showProgressPanel();
+ }
+ break;
+
+ case 'view_progress':
+ if (window.uiController && typeof window.uiController.toggleProgressPanel === 'function') {
+ window.uiController.toggleProgressPanel();
+ }
+ break;
+
+ case 'contact_support':
+ this.showSupportInfo();
+ break;
+
+ default:
+ console.log('未知のアクション:', action.action);
+ }
+ }
+
+ /**
+ * サポート情報を表示
+ */
+ showSupportInfo() {
+ const supportMessage = `
+ 問題が解決しない場合は、以下の情報をお知らせください:+ • 発生した操作
+ • エラーメッセージ
+ • 使用しているブラウザ
+ • 現在の学習進捗 + `; + + this.show(supportMessage, 'info', { + duration: 0, + actions: [ + { + label: 'エラーログをコピー', + callback: () => this.copyErrorLog(), + primary: true + }, + { + label: '閉じる', + callback: () => {}, + primary: false + } + ] + }); + } + + /** + * エラーログをクリップボードにコピー + */ + async copyErrorLog() { + try { + if (window.errorHandler) { + const errorStats = window.errorHandler.getErrorStats(); + const logText = JSON.stringify(errorStats, null, 2); + + await navigator.clipboard.writeText(logText); + this.show('エラーログをクリップボードにコピーしました', 'success'); + } else { + this.show('エラーログが利用できません', 'warning'); + } + } catch (error) { + console.error('クリップボードへのコピーに失敗:', error); + this.show('クリップボードへのコピーに失敗しました', 'error'); + } + } + + /** + * システム状態の通知を表示 + * @param {Object} healthStatus - システム健全性情報 + */ + showSystemStatus(healthStatus) { + let message = ''; + let type = 'info'; + + switch (healthStatus.overall) { + case 'healthy': + message = 'システムは正常に動作しています'; + type = 'success'; + break; + case 'degraded': + message = 'システムの一部機能に制限があります'; + type = 'warning'; + break; + case 'unhealthy': + message = 'システムに問題が発生しています'; + type = 'error'; + break; + } + + const actions = []; + if (healthStatus.overall !== 'healthy') { + actions.push({ + label: '詳細を確認', + callback: () => console.log('システム状態:', healthStatus), + primary: true + }); + } + + this.show(message, type, { + duration: type === 'success' ? 3000 : 0, + actions: actions + }); + } +} + +// グローバルインスタンスを作成 +window.notificationSystem = new NotificationSystem(); + +export { NotificationSystem }; \ No newline at end of file diff --git a/js/progress-manager.js b/js/progress-manager.js new file mode 100644 index 0000000..9f2ff71 --- /dev/null +++ b/js/progress-manager.js @@ -0,0 +1,605 @@ +/** + * ProgressManager - 進捗管理システム + * localStorageを使用した進捗データの保存・読み込み、整合性チェック、エラー処理を提供 + */ +class ProgressManager { + constructor() { + this.storageKey = 'courseProgress'; + this.selectedCourseKey = 'selectedCourse'; + this.progressData = {}; + this.initialized = false; + } + + /** + * 進捗管理システムを初期化 + */ + async initialize() { + try { + this.loadProgressData(); + this.validateProgressData(); + this.initialized = true; + console.log('進捗管理システムが初期化されました'); + return true; + } catch (error) { + console.error('進捗管理システム初期化エラー:', error); + + // ErrorHandlerを使用して進捗データ破損を処理 + if (window.errorHandler) { + const result = await window.errorHandler.handleError('PROGRESS_DATA_CORRUPTION', error, { + operation: 'initialize', + storageKey: this.storageKey + }); + + if (result.success) { + // 復旧されたデータを使用 + if (result.recoveredData) { + this.progressData = result.recoveredData; + this.initialized = true; + return true; + } + } + } + + this.handleInitializationError(error); + return false; + } + } + + /** + * 進捗データをlocalStorageから読み込み + */ + loadProgressData() { + try { + const savedData = localStorage.getItem(this.storageKey); + if (savedData) { + const parsedData = JSON.parse(savedData); + this.progressData = this.migrateProgressData(parsedData); + } else { + this.progressData = {}; + } + } catch (error) { + console.error('進捗データ読み込みエラー:', error); + throw new Error('進捗データの読み込みに失敗しました'); + } + } + + /** + * 進捗データの形式を最新版にマイグレーション + */ + migrateProgressData(data) { + // 古い形式のデータを新しい形式に変換 + if (data.courseProgress) { + // 新しい形式の場合はそのまま返す + return data.courseProgress; + } else if (typeof data === 'object' && data !== null) { + // 直接コース進捗データが格納されている場合 + return data; + } + return {}; + } + + /** + * 進捗データの整合性をチェック + */ + validateProgressData() { + const validatedData = {}; + + for (const [courseId, progress] of Object.entries(this.progressData)) { + try { + const validatedProgress = this.validateCourseProgress(courseId, progress); + if (validatedProgress) { + validatedData[courseId] = validatedProgress; + } + } catch (error) { + console.warn(`コース ${courseId} の進捗データが無効です:`, error); + // 無効なデータは除外 + } + } + + this.progressData = validatedData; + } + + /** + * 個別コースの進捗データを検証 + */ + validateCourseProgress(courseId, progress) { + if (!progress || typeof progress !== 'object') { + throw new Error('進捗データが無効な形式です'); + } + + // 必須フィールドの存在チェック + const requiredFields = ['completedLessons', 'completedModules', 'startDate', 'lastAccessed']; + for (const field of requiredFields) { + if (!(field in progress)) { + console.warn(`必須フィールド ${field} が見つかりません。初期化します。`); + progress[field] = this.getDefaultFieldValue(field); + } + } + + // データ型の検証 + if (!Array.isArray(progress.completedLessons)) { + progress.completedLessons = []; + } + if (!Array.isArray(progress.completedModules)) { + progress.completedModules = []; + } + + // 日付の検証 + if (!this.isValidDate(progress.startDate)) { + progress.startDate = new Date().toISOString(); + } + if (!this.isValidDate(progress.lastAccessed)) { + progress.lastAccessed = new Date().toISOString(); + } + + // 数値の検証 + if (typeof progress.totalScore !== 'number' || progress.totalScore < 0) { + progress.totalScore = 0; + } + + // ブール値の検証 + if (typeof progress.isCompleted !== 'boolean') { + progress.isCompleted = false; + } + + return progress; + } + + /** + * デフォルトフィールド値を取得 + */ + getDefaultFieldValue(field) { + const defaults = { + completedLessons: [], + completedModules: [], + startDate: new Date().toISOString(), + lastAccessed: new Date().toISOString(), + currentModule: null, + currentLesson: null, + totalScore: 0, + isCompleted: false + }; + return defaults[field]; + } + + /** + * 日付文字列の有効性をチェック + */ + isValidDate(dateString) { + if (!dateString || typeof dateString !== 'string') { + return false; + } + const date = new Date(dateString); + return !isNaN(date.getTime()); + } + + /** + * コースの進捗データを取得 + */ + getCourseProgress(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + return this.progressData[courseId] || null; + } + + /** + * 全ての進捗データを取得 + */ + getAllProgress() { + return { ...this.progressData }; + } + + /** + * コースの進捗を初期化 + */ + initializeCourseProgress(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + const now = new Date().toISOString(); + this.progressData[courseId] = { + currentModule: null, + currentLesson: null, + completedLessons: [], + completedModules: [], + startDate: now, + lastAccessed: now, + totalScore: 0, + isCompleted: false + }; + + this.saveProgressData(); + console.log(`コース進捗を初期化しました: ${courseId}`); + return this.progressData[courseId]; + } + + /** + * レッスン完了を記録 + */ + markLessonCompleted(courseId, lessonId, score = 0) { + if (!courseId || !lessonId) { + throw new Error('コースIDまたはレッスンIDが指定されていません'); + } + + // 進捗データが存在しない場合は初期化 + if (!this.progressData[courseId]) { + this.initializeCourseProgress(courseId); + } + + const progress = this.progressData[courseId]; + + // レッスン完了をマーク(重複チェック) + if (!progress.completedLessons.includes(lessonId)) { + progress.completedLessons.push(lessonId); + } + + // 現在のレッスンを更新 + progress.currentLesson = lessonId; + progress.lastAccessed = new Date().toISOString(); + + // スコアを加算 + if (typeof score === 'number' && score > 0) { + progress.totalScore += score; + } + + this.saveProgressData(); + console.log(`レッスン完了を記録しました: ${courseId} - ${lessonId}`); + + return progress; + } + + /** + * モジュール完了を記録 + */ + markModuleCompleted(courseId, moduleId) { + if (!courseId || !moduleId) { + throw new Error('コースIDまたはモジュールIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + throw new Error(`コース ${courseId} の進捗データが見つかりません`); + } + + const progress = this.progressData[courseId]; + + // モジュール完了をマーク(重複チェック) + if (!progress.completedModules.includes(moduleId)) { + progress.completedModules.push(moduleId); + } + + progress.currentModule = moduleId; + progress.lastAccessed = new Date().toISOString(); + + this.saveProgressData(); + console.log(`モジュール完了を記録しました: ${courseId} - ${moduleId}`); + + return progress; + } + + /** + * コース完了を記録 + */ + markCourseCompleted(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + throw new Error(`コース ${courseId} の進捗データが見つかりません`); + } + + const progress = this.progressData[courseId]; + progress.isCompleted = true; + progress.lastAccessed = new Date().toISOString(); + + this.saveProgressData(); + console.log(`コース完了を記録しました: ${courseId}`); + + return progress; + } + + /** + * 進捗データをlocalStorageに保存 + */ + saveProgressData() { + try { + const dataToSave = { + courseProgress: this.progressData, + lastSaved: new Date().toISOString(), + version: '1.0' + }; + localStorage.setItem(this.storageKey, JSON.stringify(dataToSave)); + } catch (error) { + console.error('進捗データ保存エラー:', error); + this.handleSaveError(error); + } + } + + /** + * 選択されたコースを保存 + */ + saveSelectedCourse(courseId) { + try { + localStorage.setItem(this.selectedCourseKey, courseId); + } catch (error) { + console.error('選択コース保存エラー:', error); + } + } + + /** + * 選択されたコースを取得 + */ + getSelectedCourse() { + try { + return localStorage.getItem(this.selectedCourseKey); + } catch (error) { + console.error('選択コース取得エラー:', error); + return null; + } + } + + /** + * 特定コースの進捗をリセット + */ + resetCourseProgress(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + delete this.progressData[courseId]; + this.saveProgressData(); + console.log(`コース進捗をリセットしました: ${courseId}`); + } + + /** + * 全ての進捗をリセット + */ + resetAllProgress() { + this.progressData = {}; + try { + localStorage.removeItem(this.storageKey); + localStorage.removeItem(this.selectedCourseKey); + } catch (error) { + console.error('進捗リセットエラー:', error); + } + console.log('全ての進捗をリセットしました'); + } + + /** + * 進捗統計を取得 + */ + getProgressStats(courseId) { + const progress = this.getCourseProgress(courseId); + if (!progress) { + return null; + } + + return { + completedLessonsCount: progress.completedLessons.length, + completedModulesCount: progress.completedModules.length, + totalScore: progress.totalScore, + isCompleted: progress.isCompleted, + startDate: progress.startDate, + lastAccessed: progress.lastAccessed, + daysActive: this.calculateDaysActive(progress.startDate, progress.lastAccessed) + }; + } + + /** + * アクティブ日数を計算 + */ + calculateDaysActive(startDate, lastAccessed) { + try { + const start = new Date(startDate); + const last = new Date(lastAccessed); + const diffTime = Math.abs(last - start); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; + } catch (error) { + return 0; + } + } + + /** + * 初期化エラーの処理 + */ + handleInitializationError(error) { + console.error('進捗管理システムの初期化に失敗しました:', error); + + // 破損したデータをバックアップ + try { + const corruptedData = localStorage.getItem(this.storageKey); + if (corruptedData) { + localStorage.setItem(`${this.storageKey}_backup_${Date.now()}`, corruptedData); + } + } catch (backupError) { + console.error('バックアップ作成エラー:', backupError); + } + + // 進捗データをリセット + this.progressData = {}; + this.initialized = true; + } + + /** + * 保存エラーの処理 + */ + handleSaveError(error) { + if (error.name === 'QuotaExceededError') { + console.error('localStorageの容量が不足しています'); + // 古いバックアップデータを削除 + this.cleanupOldBackups(); + } else { + console.error('進捗データの保存に失敗しました:', error); + } + } + + /** + * 古いバックアップデータを削除 + */ + cleanupOldBackups() { + try { + const keys = Object.keys(localStorage); + const backupKeys = keys.filter(key => key.startsWith(`${this.storageKey}_backup_`)); + + // 古いバックアップから削除(最新5個を保持) + backupKeys.sort().slice(0, -5).forEach(key => { + localStorage.removeItem(key); + }); + } catch (error) { + console.error('バックアップクリーンアップエラー:', error); + } + } + + /** + * 進捗データの整合性をチェック(外部から呼び出し可能) + */ + validateDataIntegrity() { + const issues = []; + + for (const [courseId, progress] of Object.entries(this.progressData)) { + // 必須フィールドのチェック + const requiredFields = ['completedLessons', 'completedModules', 'startDate', 'lastAccessed']; + for (const field of requiredFields) { + if (!(field in progress)) { + issues.push(`${courseId}: 必須フィールド ${field} が見つかりません`); + } + } + + // データ型のチェック + if (!Array.isArray(progress.completedLessons)) { + issues.push(`${courseId}: completedLessons が配列ではありません`); + } + if (!Array.isArray(progress.completedModules)) { + issues.push(`${courseId}: completedModules が配列ではありません`); + } + + // 日付の妥当性チェック + if (!this.isValidDate(progress.startDate)) { + issues.push(`${courseId}: startDate が無効な日付です`); + } + if (!this.isValidDate(progress.lastAccessed)) { + issues.push(`${courseId}: lastAccessed が無効な日付です`); + } + } + + return { + isValid: issues.length === 0, + issues: issues + }; + } + + /** + * 初期化状態を確認 + */ + isInitialized() { + return this.initialized; + } + + /** + * デバッグ情報を取得 + */ + getDebugInfo() { + return { + initialized: this.initialized, + progressDataKeys: Object.keys(this.progressData), + selectedCourse: this.getSelectedCourse(), + storageUsage: this.getStorageUsage() + }; + } + + /** + * localStorage使用量を取得 + */ + getStorageUsage() { + try { + const data = localStorage.getItem(this.storageKey); + return data ? data.length : 0; + } catch (error) { + return -1; + } + } + + /** + * 最終アクセス日時を更新 + */ + updateLastAccessed(courseId) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + this.initializeCourseProgress(courseId); + } + + this.progressData[courseId].lastAccessed = new Date().toISOString(); + this.saveProgressData(); + } + + /** + * 完了済みレッスンを設定(修復用) + */ + setCompletedLessons(courseId, lessons) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + this.initializeCourseProgress(courseId); + } + + if (!Array.isArray(lessons)) { + throw new Error('レッスンリストは配列である必要があります'); + } + + this.progressData[courseId].completedLessons = [...lessons]; + this.progressData[courseId].lastAccessed = new Date().toISOString(); + this.saveProgressData(); + } + + /** + * 完了済みモジュールを設定(修復用) + */ + setCompletedModules(courseId, modules) { + if (!courseId) { + throw new Error('コースIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + this.initializeCourseProgress(courseId); + } + + if (!Array.isArray(modules)) { + throw new Error('モジュールリストは配列である必要があります'); + } + + this.progressData[courseId].completedModules = [...modules]; + this.progressData[courseId].lastAccessed = new Date().toISOString(); + this.saveProgressData(); + } + + /** + * 完了済みモジュールを削除(修復用) + */ + removeCompletedModule(courseId, moduleId) { + if (!courseId || !moduleId) { + throw new Error('コースIDまたはモジュールIDが指定されていません'); + } + + if (!this.progressData[courseId]) { + return; + } + + const progress = this.progressData[courseId]; + const index = progress.completedModules.indexOf(moduleId); + if (index > -1) { + progress.completedModules.splice(index, 1); + progress.lastAccessed = new Date().toISOString(); + this.saveProgressData(); + } + } +} + +export { ProgressManager }; \ No newline at end of file diff --git a/js/progress-ui.js b/js/progress-ui.js new file mode 100644 index 0000000..98e69dc --- /dev/null +++ b/js/progress-ui.js @@ -0,0 +1,424 @@ +/** + * ProgressUI - 進捗表示UIの管理クラス + * 進捗インジケーター、モジュール構造表示、レッスン完了状態の可視化を担当 + */ +export class ProgressUI { + constructor(courseManager, gameEngine) { + this.courseManager = courseManager; + this.gameEngine = gameEngine; + this.isVisible = false; + this.currentCourse = null; + this.currentProgress = null; + + this.elements = { + progressPanel: document.getElementById('progress-panel'), + toggleButton: document.getElementById('toggle-progress-panel'), + showProgressBtn: document.getElementById('show-progress-btn'), + + // コース進捗要素 + currentCourseName: document.getElementById('current-course-name'), + courseProgressPercentage: document.getElementById('course-progress-percentage'), + courseProgressFill: document.getElementById('course-progress-fill'), + completedLessonsCount: document.getElementById('completed-lessons-count'), + totalLessonsCount: document.getElementById('total-lessons-count'), + + // モジュール進捗要素 + modulesList: document.getElementById('modules-list'), + + // 現在のレッスン要素 + currentModuleName: document.getElementById('current-module-name'), + currentLessonName: document.getElementById('current-lesson-name'), + prevLessonBtn: document.getElementById('prev-lesson-btn'), + nextLessonBtn: document.getElementById('next-lesson-btn'), + + // 統計要素 + totalScore: document.getElementById('total-score'), + learningDays: document.getElementById('learning-days'), + completedModulesCount: document.getElementById('completed-modules-count') + }; + + this.bindEvents(); + } + + /** + * イベントリスナーを設定 + */ + bindEvents() { + // パネル表示切り替え(ヘッダーボタン) + if (this.elements.showProgressBtn) { + this.elements.showProgressBtn.addEventListener('click', () => { + this.togglePanel(); + }); + } + + // パネル表示切り替え(パネル内ボタン) + if (this.elements.toggleButton) { + this.elements.toggleButton.addEventListener('click', () => { + this.togglePanel(); + }); + } + + // レッスンナビゲーション + if (this.elements.prevLessonBtn) { + this.elements.prevLessonBtn.addEventListener('click', () => { + this.navigateToPreviousLesson(); + }); + } + + if (this.elements.nextLessonBtn) { + this.elements.nextLessonBtn.addEventListener('click', () => { + this.navigateToNextLesson(); + }); + } + } + + /** + * 進捗パネルの表示/非表示を切り替え + */ + togglePanel() { + this.isVisible = !this.isVisible; + + if (this.isVisible) { + this.showPanel(); + } else { + this.hidePanel(); + } + } + + /** + * 進捗パネルを表示 + */ + showPanel() { + if (this.elements.progressPanel) { + this.elements.progressPanel.classList.remove('hidden'); + this.isVisible = true; + + // 現在のコースがある場合は進捗を更新 + if (this.currentCourse) { + this.updateProgressDisplay(); + } + } + } + + /** + * 進捗パネルを非表示 + */ + hidePanel() { + if (this.elements.progressPanel) { + this.elements.progressPanel.classList.add('hidden'); + this.isVisible = false; + } + } + + /** + * コースが選択された時の処理 + */ + onCourseSelected(course) { + this.currentCourse = course; + this.currentProgress = this.courseManager.getCourseProgress(course.id); + + if (this.isVisible) { + this.updateProgressDisplay(); + } + + // パネルを自動表示 + this.showPanel(); + } + + /** + * 進捗表示を更新 + */ + updateProgressDisplay() { + if (!this.currentCourse || !this.currentProgress) { + return; + } + + this.updateCourseProgress(); + this.updateModulesProgress(); + this.updateCurrentLessonInfo(); + this.updateLearningStats(); + } + + /** + * コース全体の進捗を更新 + */ + updateCourseProgress() { + const course = this.currentCourse; + const progress = this.currentProgress; + + // コース名を設定 + if (this.elements.currentCourseName) { + this.elements.currentCourseName.textContent = course.title; + } + + // 総レッスン数を計算 + const totalLessons = course.modules.reduce((total, module) => + total + module.lessons.length, 0 + ); + + const completedLessons = progress.completedLessons.length; + const progressPercentage = totalLessons > 0 ? + Math.round((completedLessons / totalLessons) * 100) : 0; + + // 進捗パーセンテージを更新 + if (this.elements.courseProgressPercentage) { + this.elements.courseProgressPercentage.textContent = `${progressPercentage}%`; + } + + // 進捗バーを更新 + if (this.elements.courseProgressFill) { + this.elements.courseProgressFill.style.width = `${progressPercentage}%`; + } + + // レッスン数を更新 + if (this.elements.completedLessonsCount) { + this.elements.completedLessonsCount.textContent = completedLessons; + } + if (this.elements.totalLessonsCount) { + this.elements.totalLessonsCount.textContent = totalLessons; + } + } + + /** + * モジュール進捗を更新 + */ + updateModulesProgress() { + const course = this.currentCourse; + const progress = this.currentProgress; + + if (!this.elements.modulesList) return; + + const modulesHtml = course.modules.map(module => { + const completedLessons = module.lessons.filter(lessonId => + progress.completedLessons.includes(lessonId) + ).length; + + const totalLessons = module.lessons.length; + const moduleProgress = totalLessons > 0 ? + Math.round((completedLessons / totalLessons) * 100) : 0; + + const isCompleted = progress.completedModules.includes(module.id); + const isCurrent = this.isCurrentModule(module.id); + + const moduleClasses = [ + 'module-item', + isCompleted ? 'completed' : '', + isCurrent ? 'current' : '' + ].filter(Boolean).join(' '); + + const statusIcon = isCompleted ? '✅' : + isCurrent ? '📍' : '⏳'; + + // レッスンドットを生成 + const lessonDots = module.lessons.map(lessonId => { + const isLessonCompleted = progress.completedLessons.includes(lessonId); + const isCurrentLesson = this.isCurrentLesson(lessonId); + + const dotClasses = [ + 'lesson-dot', + isLessonCompleted ? 'completed' : '', + isCurrentLesson ? 'current' : '' + ].filter(Boolean).join(' '); + + return ``; + }).join(''); + + return ` +
+
+ `;
+ }).join('');
+
+ this.elements.modulesList.innerHTML = modulesHtml;
+
+ // レッスンドットのクリックイベントを設定
+ this.bindLessonDotEvents();
+ }
+
+ /**
+ * 現在のレッスン情報を更新
+ */
+ updateCurrentLessonInfo() {
+ const nextLesson = this.courseManager.getNextLesson(this.currentCourse.id);
+
+ if (nextLesson) {
+ // 現在のモジュール名を設定
+ if (this.elements.currentModuleName) {
+ this.elements.currentModuleName.textContent = nextLesson.moduleTitle;
+ }
+
+ // 現在のレッスン名を設定
+ if (this.elements.currentLessonName) {
+ this.elements.currentLessonName.textContent = nextLesson.lessonId;
+ }
+ } else {
+ // 全レッスン完了の場合
+ if (this.elements.currentModuleName) {
+ this.elements.currentModuleName.textContent = 'コース完了';
+ }
+ if (this.elements.currentLessonName) {
+ this.elements.currentLessonName.textContent = 'おめでとうございます!';
+ }
+ }
+
+ // ナビゲーションボタンの状態を更新
+ this.updateNavigationButtons();
+ }
+
+ /**
+ * 学習統計を更新
+ */
+ updateLearningStats() {
+ const stats = this.courseManager.getProgressStats(this.currentCourse.id);
+
+ if (stats) {
+ // 総スコア
+ if (this.elements.totalScore) {
+ this.elements.totalScore.textContent = stats.totalScore.toLocaleString();
+ }
+
+ // 学習日数
+ if (this.elements.learningDays) {
+ this.elements.learningDays.textContent = stats.daysActive;
+ }
+
+ // 完了モジュール数
+ if (this.elements.completedModulesCount) {
+ this.elements.completedModulesCount.textContent = stats.completedModulesCount;
+ }
+ }
+ }
+
+ /**
+ * ナビゲーションボタンの状態を更新
+ */
+ updateNavigationButtons() {
+ // 前のレッスンボタン
+ const hasPreviousLesson = this.currentProgress.completedLessons.length > 0;
+ if (this.elements.prevLessonBtn) {
+ this.elements.prevLessonBtn.disabled = !hasPreviousLesson;
+ }
+
+ // 次のレッスンボタン
+ const nextLesson = this.courseManager.getNextLesson(this.currentCourse.id);
+ if (this.elements.nextLessonBtn) {
+ this.elements.nextLessonBtn.disabled = !nextLesson;
+ }
+ }
+
+ /**
+ * レッスンドットのクリックイベントを設定
+ */
+ bindLessonDotEvents() {
+ const lessonDots = this.elements.modulesList.querySelectorAll('.lesson-dot');
+ lessonDots.forEach(dot => {
+ dot.addEventListener('click', (e) => {
+ const lessonId = dot.dataset.lessonId;
+ this.navigateToLesson(lessonId);
+ });
+ });
+ }
+
+ /**
+ * 指定されたレッスンに移動
+ */
+ navigateToLesson(lessonId) {
+ // レッスンがアンロックされているかチェック
+ if (!this.courseManager.isLessonUnlocked(this.currentCourse.id, lessonId)) {
+ alert('このレッスンはまだアンロックされていません。');
+ return;
+ }
+
+ // GameEngineに現在のレッスンを設定
+ if (this.gameEngine && typeof this.gameEngine.setCurrentLesson === 'function') {
+ this.gameEngine.setCurrentLesson(lessonId);
+ }
+
+ // UIControllerにレッスン変更を通知
+ if (window.uiController && typeof window.uiController.updateChallenge === 'function') {
+ window.uiController.updateChallenge();
+ }
+ }
+
+ /**
+ * 前のレッスンに移動
+ */
+ navigateToPreviousLesson() {
+ const completedLessons = this.currentProgress.completedLessons;
+ if (completedLessons.length > 0) {
+ const previousLessonId = completedLessons[completedLessons.length - 1];
+ this.navigateToLesson(previousLessonId);
+ }
+ }
+
+ /**
+ * 次のレッスンに移動
+ */
+ navigateToNextLesson() {
+ const nextLesson = this.courseManager.getNextLesson(this.currentCourse.id);
+ if (nextLesson) {
+ this.navigateToLesson(nextLesson.lessonId);
+ }
+ }
+
+ /**
+ * 指定されたモジュールが現在のモジュールかチェック
+ */
+ isCurrentModule(moduleId) {
+ const nextLesson = this.courseManager.getNextLesson(this.currentCourse.id);
+ return nextLesson && nextLesson.moduleId === moduleId;
+ }
+
+ /**
+ * 指定されたレッスンが現在のレッスンかチェック
+ */
+ isCurrentLesson(lessonId) {
+ const nextLesson = this.courseManager.getNextLesson(this.currentCourse.id);
+ return nextLesson && nextLesson.lessonId === lessonId;
+ }
+
+ /**
+ * 進捗が更新された時の処理
+ */
+ onProgressUpdated() {
+ if (this.currentCourse) {
+ this.currentProgress = this.courseManager.getCourseProgress(this.currentCourse.id);
+ if (this.isVisible) {
+ this.updateProgressDisplay();
+ }
+ }
+ }
+
+ /**
+ * パネルの表示状態を取得
+ */
+ getVisibilityState() {
+ return this.isVisible;
+ }
+
+ /**
+ * 強制的に進捗表示を更新
+ */
+ forceUpdate() {
+ if (this.currentCourse && this.isVisible) {
+ this.currentProgress = this.courseManager.getCourseProgress(this.currentCourse.id);
+ this.updateProgressDisplay();
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/ui-controller.js b/js/ui-controller.js
index 39d9140..7c36891 100644
--- a/js/ui-controller.js
+++ b/js/ui-controller.js
@@ -7,6 +7,8 @@ export class UIController {
this.sqlTokenizer = null;
this.currentChallengeType = null; // 現在選択されている問題タイプ
this.isMobile = this.detectMobileDevice(); // モバイルデバイス検出
+ this.courseManager = null; // CourseManagerの参照
+ this.currentCourse = null; // 現在選択されているコース
this.initializeElements();
this.bindEvents();
// ゲームオーバーレイを初期状態で非表示
@@ -84,6 +86,12 @@ export class UIController {
this.elements.toggleSidebar.addEventListener('click', () => this.toggleSidebar());
+ // コース切り替えボタンのイベントリスナー
+ const switchCourseBtn = document.getElementById('switch-course-btn');
+ if (switchCourseBtn) {
+ switchCourseBtn.addEventListener('click', () => this.switchCourse());
+ }
+
// 問題タイプ選択のイベントリスナーを追加
this.bindChallengeTypeEvents();
}
@@ -261,6 +269,383 @@ export class UIController {
updateChallenge() {
// 新しい問題タイプ選択機能付きのメソッドを呼び出し
this.updateChallengeWithTypeSelection();
+
+ // コースシステムが有効な場合は追加の更新処理を実行
+ if (this.courseManager && this.currentCourse) {
+ this.updateCourseDisplay();
+ }
+ }
+
+ /**
+ * コース選択時の処理
+ */
+ onCourseSelected(course) {
+ console.log(`UIController: コースが選択されました - ${course.title}`);
+ this.currentCourse = course;
+ this.updateCourseInfo(course);
+
+ // コース選択画面を非表示にしてメイン画面を表示
+ this.hideCourseSelection();
+
+ // コース表示を更新
+ this.updateCourseDisplay();
+
+ // チャレンジを更新
+ this.updateChallenge();
+ }
+
+ /**
+ * コース情報を更新
+ */
+ updateCourseInfo(course) {
+ const courseInfo = document.getElementById('current-course-info');
+ const courseTitle = document.getElementById('current-course-title');
+
+ if (courseInfo && courseTitle) {
+ if (course) {
+ courseTitle.textContent = course.title;
+ courseInfo.classList.remove('hidden');
+ } else {
+ courseInfo.classList.add('hidden');
+ }
+ }
+ }
+
+ /**
+ * コース切り替え処理
+ */
+ switchCourse() {
+ if (window.courseUI && typeof window.courseUI.switchCourse === 'function') {
+ window.courseUI.switchCourse();
+ }
+ }
+
+ /**
+ * CourseManagerを設定
+ * @param {CourseManager} courseManager - CourseManagerのインスタンス
+ */
+ setCourseManager(courseManager) {
+ this.courseManager = courseManager;
+ }
+
+ /**
+ * コースシステムの初期化
+ */
+ initializeCourseUI() {
+ // コース関連のUI要素を初期化
+ this.updateCourseDisplay();
+
+ // 進捗表示ボタンのイベントリスナー
+ const showProgressBtn = document.getElementById('show-progress-btn');
+ if (showProgressBtn) {
+ showProgressBtn.addEventListener('click', () => this.toggleProgressPanel());
+ }
+
+ // 進捗パネルの閉じるボタン
+ const toggleProgressPanel = document.getElementById('toggle-progress-panel');
+ if (toggleProgressPanel) {
+ toggleProgressPanel.addEventListener('click', () => this.toggleProgressPanel());
+ }
+
+ console.log('コースUI初期化完了');
+ }
+
+ /**
+ * コース表示を更新
+ */
+ updateCourseDisplay() {
+ if (!this.courseManager) return;
+
+ const currentCourse = this.courseManager.getCurrentCourse();
+ this.currentCourse = currentCourse;
+
+ // ヘッダーのコース情報を更新
+ this.updateCourseInfo(currentCourse);
+
+ // 進捗情報を更新
+ this.updateProgressDisplay();
+
+ // ナビゲーションを更新
+ this.updateCourseNavigation();
+ }
+
+ /**
+ * 進捗表示を更新
+ */
+ updateProgressDisplay() {
+ if (!this.courseManager || !this.currentCourse) return;
+
+ const progress = this.courseManager.getCourseProgress(this.currentCourse.id);
+ if (!progress) return;
+
+ // コース名を更新
+ const courseNameElement = document.getElementById('current-course-name');
+ if (courseNameElement) {
+ courseNameElement.textContent = this.currentCourse.title;
+ }
+
+ // 進捗パーセンテージを計算
+ const totalLessons = this.currentCourse.modules.reduce((total, module) =>
+ total + module.lessons.length, 0
+ );
+ const completedLessons = progress.completedLessons.length;
+ const progressPercentage = totalLessons > 0 ?
+ Math.round((completedLessons / totalLessons) * 100) : 0;
+
+ // 進捗バーを更新
+ const progressFillElement = document.getElementById('course-progress-fill');
+ const progressPercentageElement = document.getElementById('course-progress-percentage');
+ const completedLessonsElement = document.getElementById('completed-lessons-count');
+ const totalLessonsElement = document.getElementById('total-lessons-count');
+
+ if (progressFillElement) {
+ progressFillElement.style.width = `${progressPercentage}%`;
+ }
+ if (progressPercentageElement) {
+ progressPercentageElement.textContent = `${progressPercentage}%`;
+ }
+ if (completedLessonsElement) {
+ completedLessonsElement.textContent = completedLessons;
+ }
+ if (totalLessonsElement) {
+ totalLessonsElement.textContent = totalLessons;
+ }
+
+ // モジュール進捗を更新
+ this.updateModulesDisplay();
+
+ // 現在のレッスン情報を更新
+ this.updateCurrentLessonDisplay();
+
+ // 学習統計を更新
+ this.updateLearningStats();
+ }
+
+ /**
+ * モジュール表示を更新
+ */
+ updateModulesDisplay() {
+ if (!this.courseManager || !this.currentCourse) return;
+
+ const progress = this.courseManager.getCourseProgress(this.currentCourse.id);
+ if (!progress) return;
+
+ const modulesList = document.getElementById('modules-list');
+ if (!modulesList) return;
+
+ const modulesHtml = this.currentCourse.modules.map(module => {
+ const isCompleted = progress.completedModules.includes(module.id);
+ const completedLessonsInModule = module.lessons.filter(lessonId =>
+ progress.completedLessons.includes(lessonId)
+ ).length;
+ const moduleProgress = module.lessons.length > 0 ?
+ Math.round((completedLessonsInModule / module.lessons.length) * 100) : 0;
+
+ const isUnlocked = this.courseManager.isModuleUnlocked(this.currentCourse.id, module.id);
+
+ return `
+
+
+
+ ${statusIcon}
+ ${module.title}
+
+
+ ${completedLessons}/${totalLessons}
+
+
+
+
+ ${moduleProgress}%
+
+ ${lessonDots}
+
+
+
+ `;
+ }).join('');
+
+ modulesList.innerHTML = modulesHtml;
+ }
+
+ /**
+ * 現在のレッスン表示を更新
+ */
+ updateCurrentLessonDisplay() {
+ if (!this.courseManager || !this.currentCourse) return;
+
+ const currentChallenge = this.gameEngine.getCurrentChallenge();
+ if (!currentChallenge) return;
+
+ // 現在のチャレンジがどのモジュールに属するかを特定
+ let currentModule = null;
+ for (const module of this.currentCourse.modules) {
+ if (module.lessons.includes(currentChallenge.id)) {
+ currentModule = module;
+ break;
+ }
+ }
+
+ const currentModuleElement = document.getElementById('current-module-name');
+ const currentLessonElement = document.getElementById('current-lesson-name');
+
+ if (currentModuleElement && currentModule) {
+ currentModuleElement.textContent = currentModule.title;
+ }
+ if (currentLessonElement) {
+ currentLessonElement.textContent = currentChallenge.title || currentChallenge.id;
+ }
+
+ // レッスンナビゲーションボタンの状態を更新
+ this.updateLessonNavigationButtons();
+ }
+
+ /**
+ * レッスンナビゲーションボタンの状態を更新
+ */
+ updateLessonNavigationButtons() {
+ const prevLessonBtn = document.getElementById('prev-lesson-btn');
+ const nextLessonBtn = document.getElementById('next-lesson-btn');
+
+ if (prevLessonBtn) {
+ prevLessonBtn.disabled = !this.gameEngine.canGoPrevious();
+ prevLessonBtn.addEventListener('click', () => this.previousChallenge());
+ }
+
+ if (nextLessonBtn) {
+ nextLessonBtn.disabled = !this.gameEngine.canGoNext();
+ nextLessonBtn.addEventListener('click', () => this.nextChallenge());
+ }
+ }
+
+ /**
+ * 学習統計を更新
+ */
+ updateLearningStats() {
+ if (!this.courseManager || !this.currentCourse) return;
+
+ const progress = this.courseManager.getCourseProgress(this.currentCourse.id);
+ if (!progress) return;
+
+ const totalScoreElement = document.getElementById('total-score');
+ const learningDaysElement = document.getElementById('learning-days');
+ const completedModulesElement = document.getElementById('completed-modules-count');
+
+ if (totalScoreElement) {
+ totalScoreElement.textContent = progress.totalScore || 0;
+ }
+
+ if (learningDaysElement && progress.startDate) {
+ const startDate = new Date(progress.startDate);
+ const currentDate = new Date();
+ const daysDiff = Math.ceil((currentDate - startDate) / (1000 * 60 * 60 * 24));
+ learningDaysElement.textContent = daysDiff;
+ }
+
+ if (completedModulesElement) {
+ completedModulesElement.textContent = progress.completedModules.length;
+ }
+ }
+
+ /**
+ * 進捗パネルの表示/非表示を切り替え
+ */
+ toggleProgressPanel() {
+ const progressPanel = document.getElementById('progress-panel');
+ const toggleButton = document.getElementById('toggle-progress-panel');
+
+ if (progressPanel) {
+ const isHidden = progressPanel.classList.contains('hidden');
+
+ if (isHidden) {
+ progressPanel.classList.remove('hidden');
+ if (toggleButton) toggleButton.textContent = '→';
+ // 進捗情報を更新
+ this.updateProgressDisplay();
+ } else {
+ progressPanel.classList.add('hidden');
+ if (toggleButton) toggleButton.textContent = '←';
+ }
+ }
+ }
+
+ /**
+ * コースナビゲーションを更新
+ */
+ updateCourseNavigation() {
+ if (!this.courseManager || !this.currentCourse) return;
+
+ // 現在のチャレンジに基づいてナビゲーションボタンの状態を更新
+ const progress = this.gameEngine.getProgress();
+
+ // 前/次ボタンの状態を更新
+ this.elements.prevButton.disabled = progress.current === 1;
+ this.elements.nextButton.disabled = progress.current === progress.total;
+
+ // コース固有のナビゲーション情報を表示
+ this.updateCourseSpecificNavigation();
+ }
+
+ /**
+ * コース固有のナビゲーション情報を更新
+ */
+ updateCourseSpecificNavigation() {
+ if (!this.courseManager || !this.currentCourse) return;
+
+ const currentChallenge = this.gameEngine.getCurrentChallenge();
+ if (!currentChallenge) return;
+
+ // 次のレッスンがアンロックされているかチェック
+ const nextLesson = this.courseManager.getNextLesson(this.currentCourse.id);
+
+ // 次のレッスンボタンの表示を調整
+ if (nextLesson) {
+ const isNextLessonUnlocked = this.courseManager.isLessonUnlocked(
+ this.currentCourse.id,
+ nextLesson.lessonId
+ );
+
+ if (!isNextLessonUnlocked) {
+ this.elements.nextButton.disabled = true;
+ this.elements.nextButton.title = '前のレッスンを完了してください';
+ }
+ }
+ }
+
+ /**
+ * コース選択処理
+ */
+ handleCourseSelection() {
+ // コース選択画面を表示
+ this.showCourseSelection();
+ }
+
+ /**
+ * コース選択画面を表示
+ */
+ showCourseSelection() {
+ const courseSelectionScreen = document.getElementById('course-selection-screen');
+ const appLayout = document.querySelector('.app-layout');
+
+ if (courseSelectionScreen && appLayout) {
+ courseSelectionScreen.classList.remove('hidden');
+ appLayout.style.display = 'none';
+ }
+ }
+
+ /**
+ * コース選択画面を非表示
+ */
+ hideCourseSelection() {
+ const courseSelectionScreen = document.getElementById('course-selection-screen');
+ const appLayout = document.querySelector('.app-layout');
+
+ if (courseSelectionScreen && appLayout) {
+ courseSelectionScreen.classList.add('hidden');
+ appLayout.style.display = 'flex';
+ }
}
async executeQuery() {
@@ -286,6 +671,8 @@ export class UIController {
this.displayResults(result);
this.updateScore();
+ // コース進捗は GameEngine.checkAnswer() 内で既に更新されている
+
// 次の問題へのボタンを有効化
if (this.gameEngine.currentChallengeIndex < this.gameEngine.challenges.length - 1) {
this.elements.nextButton.disabled = false;
@@ -1320,4 +1707,86 @@ export class UIController {
this.elements.sqlEditor.value = '';
this.clearResults();
}
+
+ /**
+ * コース完了時の処理
+ * @param {Object} completionResult - コース完了結果
+ */
+ onCourseCompleted(completionResult) {
+ console.log('UIController: コース完了イベントを受信:', completionResult);
+
+ // 進捗UIを更新
+ if (window.progressUI && typeof window.progressUI.onCourseCompleted === 'function') {
+ window.progressUI.onCourseCompleted(completionResult);
+ }
+
+ // 現在のチャレンジを一時停止
+ this.pauseCurrentChallenge();
+
+ // コース完了の通知を表示
+ this.showCourseCompletionNotification(completionResult);
+ }
+
+ /**
+ * 現在のチャレンジを一時停止
+ */
+ pauseCurrentChallenge() {
+ // 実行中の処理があれば停止
+ if (this.elements.runButton) {
+ this.elements.runButton.disabled = true;
+ }
+
+ // ヒントパネルを閉じる
+ this.hideHint();
+
+ // 結果をクリア
+ this.clearResults();
+ }
+
+ /**
+ * コース完了通知を表示
+ * @param {Object} completionResult - コース完了結果
+ */
+ showCourseCompletionNotification(completionResult) {
+ // 簡単な通知バナーを表示
+ const notification = document.createElement('div');
+ notification.className = 'course-completion-notification';
+ notification.innerHTML = `
+
+
+
+ ${isCompleted ? '✅' : (isUnlocked ? '📚' : '🔒')}
+ ${module.title}
+
+
+ ${completedLessonsInModule}/${module.lessons.length}
+ ${moduleProgress}%
+
+
+
+
+ ${module.description}
+ ${module.prerequisites.length > 0 ?
+ `前提: ${module.prerequisites.join(', ')}
` :
+ ''
+ }
+
+
+ `;
+
+ document.body.appendChild(notification);
+
+ // 自動で5秒後に削除
+ setTimeout(() => {
+ if (notification.parentNode) {
+ document.body.removeChild(notification);
+ }
+ }, 5000);
+
+ // 閉じるボタンのイベント
+ const closeBtn = notification.querySelector('.notification-close');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => {
+ if (notification.parentNode) {
+ document.body.removeChild(notification);
+ }
+ });
+ }
+
+ // 通知をクリックで詳細表示
+ notification.addEventListener('click', (e) => {
+ if (e.target !== closeBtn) {
+ if (window.courseUI && typeof window.courseUI.showCourseCompletionModal === 'function') {
+ window.courseUI.showCourseCompletionModal(completionResult);
+ }
+ }
+ });
+ }
}
\ No newline at end of file
diff --git a/slides/big-data-basics-challenges.json b/slides/big-data-basics-challenges.json
new file mode 100644
index 0000000..d94ebc1
--- /dev/null
+++ b/slides/big-data-basics-challenges.json
@@ -0,0 +1,208 @@
+[
+ {
+ "type": "slide",
+ "title": "DuckDBの特徴と利点",
+ "content": "# DuckDBの特徴と利点\n\nDuckDBは**分析処理に特化**したSQLデータベースエンジンです。\n\n## 主な特徴\n\n- **OLAP(分析処理)に最適化**\n- **カラムナー(列指向)ストレージ**\n- **ベクトル化実行エンジン**\n- **埋め込み可能**(サーバー不要)\n- **標準SQLサポート**\n\n## 従来のRDBMSとの違い\n\n| 特徴 | 従来のRDBMS | DuckDB |\n|------|-------------|--------|\n| 用途 | OLTP(トランザクション処理) | OLAP(分析処理) |\n| ストレージ | 行指向 | 列指向 |\n| 最適化 | 更新・削除 | 集計・分析 |\n\n### DuckDBが得意な処理\n\n- 大量データの集計\n- 複雑な分析クエリ\n- データウェアハウス処理\n- ETL(Extract, Transform, Load)"
+ },
+ {
+ "type": "word-reorder",
+ "id": "duckdb-001",
+ "title": "DuckDBの基本情報を確認しよう",
+ "description": "DuckDBのバージョン情報を確認してください。",
+ "difficulty": 1,
+ "hints": [
+ "PRAGMA version; を使用してバージョンを確認できます",
+ "DuckDBの基本的な動作確認です"
+ ],
+ "solution": "PRAGMA version"
+ },
+ {
+ "type": "word-reorder",
+ "id": "duckdb-002",
+ "title": "メモリ使用量を確認しよう",
+ "description": "現在のメモリ使用量を確認してください。",
+ "difficulty": 2,
+ "hints": [
+ "PRAGMA memory_limit; でメモリ制限を確認できます",
+ "PRAGMA database_size; でデータベースサイズを確認できます"
+ ],
+ "solution": "PRAGMA database_size"
+ },
+ {
+ "type": "slide",
+ "title": "CSVファイルの読み込み",
+ "content": "# CSVファイルの読み込み\n\nDuckDBは**様々なファイル形式**を直接読み込むことができます。\n\n## CSVファイルの読み込み方法\n\n```sql\n-- 直接クエリ\nSELECT * FROM 'data/file.csv';\n\n-- read_csv関数を使用\nSELECT * FROM read_csv('data/file.csv');\n\n-- オプション指定\nSELECT * FROM read_csv('data/file.csv', \n header=true, \n delimiter=',',\n quote='\"'\n);\n```\n\n## 主なオプション\n\n- **header**: ヘッダー行の有無\n- **delimiter**: 区切り文字\n- **quote**: 引用符\n- **skip**: スキップする行数\n- **columns**: カラム定義\n\n### 実例\n\n```sql\nSELECT COUNT(*) FROM 'data/products.csv';\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "etl-001",
+ "title": "CSVファイルから商品数を取得しよう",
+ "description": "data/products.csvファイルから商品の総数を取得してください。",
+ "difficulty": 2,
+ "hints": [
+ "ファイルパスを直接FROM句で指定できます",
+ "COUNT(*)で行数をカウントします",
+ "シングルクォートでファイルパスを囲みます"
+ ],
+ "solution": "SELECT COUNT(*) FROM 'data/products.csv'"
+ },
+ {
+ "type": "word-reorder",
+ "id": "etl-002",
+ "title": "CSVファイルから高額商品を抽出しよう",
+ "description": "data/products.csvファイルから価格が10000以上の商品の商品名と価格を取得してください。",
+ "difficulty": 3,
+ "hints": [
+ "WHERE句で価格の条件を指定します",
+ "CSVファイルのカラム名を正確に指定します",
+ "price >= 10000の条件を使用します"
+ ],
+ "solution": "SELECT product_name, price FROM 'data/products.csv' WHERE price >= 10000"
+ },
+ {
+ "type": "slide",
+ "title": "複数ファイルの一括処理",
+ "content": "# 複数ファイルの一括処理\n\nDuckDBは**ワイルドカード**を使用して複数ファイルを一括処理できます。\n\n## ワイルドカードパターン\n\n```sql\n-- 同じディレクトリの全CSVファイル\nSELECT * FROM 'data/*.csv';\n\n-- 特定パターンのファイル\nSELECT * FROM 'data/sales_*.csv';\n\n-- 再帰的にサブディレクトリも含める\nSELECT * FROM 'data/**/*.csv';\n```\n\n## UNION ALLとの組み合わせ\n\n```sql\nSELECT 'products' as source, * FROM 'data/products.csv'\nUNION ALL\nSELECT 'categories' as source, * FROM 'data/categories.csv';\n```\n\n### ファイル情報の取得\n\n```sql\nSELECT filename, COUNT(*) as row_count\nFROM 'data/*.csv'\nGROUP BY filename;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "etl-003",
+ "title": "複数CSVファイルの行数を集計しよう",
+ "description": "dataディレクトリ内の全CSVファイルについて、ファイル名と行数を表示してください。",
+ "difficulty": 4,
+ "hints": [
+ "ワイルドカード 'data/*.csv' を使用します",
+ "filename関数でファイル名を取得できます",
+ "GROUP BY filename で集計します",
+ "COUNT(*)で行数をカウントします"
+ ],
+ "solution": "SELECT filename, COUNT(*) as row_count FROM 'data/*.csv' GROUP BY filename"
+ },
+ {
+ "type": "slide",
+ "title": "高度な集計関数",
+ "content": "# 高度な集計関数\n\nDuckDBは**高度な分析関数**を豊富に提供しています。\n\n## 統計関数\n\n- **STDDEV()**: 標準偏差\n- **VARIANCE()**: 分散\n- **MEDIAN()**: 中央値\n- **MODE()**: 最頻値\n- **PERCENTILE_CONT()**: パーセンタイル\n\n## 配列集約関数\n\n- **ARRAY_AGG()**: 配列に集約\n- **STRING_AGG()**: 文字列に集約\n- **LIST()**: リストに集約\n\n### 実例\n\n```sql\nSELECT \n AVG(price) as avg_price,\n MEDIAN(price) as median_price,\n STDDEV(price) as stddev_price,\n PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY price) as p95_price\nFROM products;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "analytics-001",
+ "title": "商品価格の統計情報を計算しよう",
+ "description": "商品価格の平均値、中央値、標準偏差、95パーセンタイルを計算してください。",
+ "difficulty": 5,
+ "hints": [
+ "AVG()で平均値を計算します",
+ "MEDIAN()で中央値を計算します",
+ "STDDEV()で標準偏差を計算します",
+ "PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY price)で95パーセンタイルを計算します"
+ ],
+ "solution": "SELECT AVG(price) as avg_price, MEDIAN(price) as median_price, STDDEV(price) as stddev_price, PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY price) as p95_price FROM products"
+ },
+ {
+ "type": "slide",
+ "title": "ウィンドウ関数の応用",
+ "content": "# ウィンドウ関数の応用\n\nDuckDBは**高度なウィンドウ関数**をサポートしています。\n\n## 移動平均・累積計算\n\n```sql\n-- 移動平均(3行)\nSELECT price,\n AVG(price) OVER (ORDER BY price ROWS 2 PRECEDING) as moving_avg\nFROM products;\n\n-- 累積合計\nSELECT price,\n SUM(price) OVER (ORDER BY price) as cumulative_sum\nFROM products;\n```\n\n## LAG/LEAD関数\n\n```sql\n-- 前の行の値\nSELECT price,\n LAG(price, 1) OVER (ORDER BY price) as prev_price,\n price - LAG(price, 1) OVER (ORDER BY price) as price_diff\nFROM products;\n```\n\n## NTILE関数\n\n```sql\n-- データを4分割\nSELECT product_name, price,\n NTILE(4) OVER (ORDER BY price) as quartile\nFROM products;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "analytics-002",
+ "title": "商品を価格帯で4分割しよう",
+ "description": "商品を価格順に4つのグループ(四分位)に分割し、商品名、価格、四分位を表示してください。",
+ "difficulty": 6,
+ "hints": [
+ "NTILE(4)関数を使用します",
+ "OVER (ORDER BY price)で価格順に並べます",
+ "四分位は1から4の値になります"
+ ],
+ "solution": "SELECT product_name, price, NTILE(4) OVER (ORDER BY price) as quartile FROM products"
+ },
+ {
+ "type": "word-reorder",
+ "id": "analytics-003",
+ "title": "価格の移動平均を計算しよう",
+ "description": "商品を価格順に並べて、各商品の価格と直前2商品を含む3商品の移動平均を計算してください。",
+ "difficulty": 7,
+ "hints": [
+ "AVG()ウィンドウ関数を使用します",
+ "ROWS 2 PRECEDINGで直前2行を含む範囲を指定します",
+ "ORDER BY priceで価格順に並べます"
+ ],
+ "solution": "SELECT product_name, price, AVG(price) OVER (ORDER BY price ROWS 2 PRECEDING) as moving_avg FROM products"
+ },
+ {
+ "type": "slide",
+ "title": "パフォーマンス最適化",
+ "content": "# パフォーマンス最適化\n\nDuckDBで**大規模データを効率的に処理**するための技術です。\n\n## クエリ最適化のポイント\n\n1. **列の選択**: 必要なカラムのみ選択\n2. **早期フィルタリング**: WHERE句を効果的に使用\n3. **適切なJOIN順序**: 小さなテーブルから結合\n4. **インデックス活用**: 頻繁に検索するカラムにインデックス\n\n## EXPLAIN文でクエリプランを確認\n\n```sql\nEXPLAIN SELECT * FROM products WHERE price > 10000;\n```\n\n## メモリ設定の調整\n\n```sql\nPRAGMA memory_limit='4GB';\nPRAGMA threads=4;\n```\n\n### パーティション化\n\n```sql\n-- 年月でパーティション化\nCREATE TABLE sales_partitioned AS \nSELECT *, \n YEAR(order_date) as year,\n MONTH(order_date) as month\nFROM sales;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "performance-001",
+ "title": "クエリの実行計画を確認しよう",
+ "description": "価格が10000以上の商品を検索するクエリの実行計画を確認してください。",
+ "difficulty": 4,
+ "hints": [
+ "EXPLAIN文を使用します",
+ "SELECT文の前にEXPLAINを付けます",
+ "実行計画でパフォーマンスを分析できます"
+ ],
+ "solution": "EXPLAIN SELECT * FROM products WHERE price >= 10000"
+ },
+ {
+ "type": "word-reorder",
+ "id": "performance-002",
+ "title": "メモリ制限を設定しよう",
+ "description": "DuckDBのメモリ制限を2GBに設定してください。",
+ "difficulty": 3,
+ "hints": [
+ "PRAGMA memory_limit文を使用します",
+ "メモリサイズは文字列で指定します",
+ "'2GB'のように単位を含めて指定します"
+ ],
+ "solution": "PRAGMA memory_limit='2GB'"
+ },
+ {
+ "type": "slide",
+ "title": "ETLパイプライン設計",
+ "content": "# ETLパイプライン設計\n\n**Extract, Transform, Load**の一連の処理を効率的に実行します。\n\n## ETLの基本フロー\n\n1. **Extract(抽出)**: データソースからデータを取得\n2. **Transform(変換)**: データの加工・クレンジング\n3. **Load(読み込み)**: 変換後のデータを保存\n\n## DuckDBでのETL実装\n\n```sql\n-- Extract: CSVファイルから読み込み\nCREATE TABLE raw_data AS \nSELECT * FROM 'data/raw_sales.csv';\n\n-- Transform: データクレンジング\nCREATE TABLE clean_data AS\nSELECT \n UPPER(TRIM(customer_name)) as customer_name,\n CAST(order_date as DATE) as order_date,\n CAST(amount as DECIMAL(10,2)) as amount\nFROM raw_data\nWHERE amount > 0 AND customer_name IS NOT NULL;\n\n-- Load: 最終テーブルに挿入\nINSERT INTO sales_summary \nSELECT customer_name, SUM(amount) as total_amount\nFROM clean_data\nGROUP BY customer_name;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "pipeline-001",
+ "title": "データクレンジングを実行しよう",
+ "description": "顧客名を大文字に変換し、空白を除去し、金額を小数点2桁の数値に変換してください。元データはraw_salesテーブルから取得してください。",
+ "difficulty": 6,
+ "hints": [
+ "UPPER()で大文字に変換します",
+ "TRIM()で空白を除去します",
+ "CAST(amount as DECIMAL(10,2))で数値型に変換します",
+ "WHERE句でNULLや無効なデータを除外します"
+ ],
+ "solution": "SELECT UPPER(TRIM(customer_name)) as customer_name, CAST(order_date as DATE) as order_date, CAST(amount as DECIMAL(10,2)) as amount FROM raw_sales WHERE amount > 0 AND customer_name IS NOT NULL"
+ },
+ {
+ "type": "word-reorder",
+ "id": "pipeline-002",
+ "title": "集計データを作成しよう",
+ "description": "クレンジング済みデータから顧客別の総購入金額を集計し、購入金額の多い順に並べてください。",
+ "difficulty": 5,
+ "hints": [
+ "GROUP BY customer_nameで顧客別に集計します",
+ "SUM(amount)で総購入金額を計算します",
+ "ORDER BY total_amount DESCで降順に並べます"
+ ],
+ "solution": "SELECT customer_name, SUM(amount) as total_amount FROM clean_sales GROUP BY customer_name ORDER BY total_amount DESC"
+ },
+ {
+ "type": "word-reorder",
+ "id": "pipeline-003",
+ "title": "月次売上レポートを作成しよう",
+ "description": "注文データから月別の売上合計を計算し、年月と売上金額を表示してください。2023年のデータのみを対象としてください。",
+ "difficulty": 7,
+ "hints": [
+ "YEAR()とMONTH()関数で年月を抽出します",
+ "SUM()で売上を合計します",
+ "WHERE YEAR(order_date) = 2023で2023年に限定します",
+ "GROUP BY年月で集計します"
+ ],
+ "solution": "SELECT YEAR(order_date) as year, MONTH(order_date) as month, SUM(amount) as monthly_sales FROM orders WHERE YEAR(order_date) = 2023 GROUP BY YEAR(order_date), MONTH(order_date) ORDER BY year, month"
+ }
+]
\ No newline at end of file
diff --git a/slides/db-fundamentals-challenges.json b/slides/db-fundamentals-challenges.json
new file mode 100644
index 0000000..5ba962e
--- /dev/null
+++ b/slides/db-fundamentals-challenges.json
@@ -0,0 +1,178 @@
+[
+ {
+ "type": "slide",
+ "title": "DDL(データ定義言語)の基本",
+ "content": "# DDL(データ定義言語)の基本\n\nDDL(Data Definition Language)は**データベース構造を定義**するためのSQL文です。\n\n## 主なDDL文\n\n- **CREATE**: テーブル、インデックス、ビューなどを作成\n- **ALTER**: 既存のテーブル構造を変更\n- **DROP**: テーブル、インデックス、ビューなどを削除\n- **TRUNCATE**: テーブルのデータを全削除\n\n## CREATE TABLEの基本構文\n\n```sql\nCREATE TABLE テーブル名 (\n カラム名1 データ型 制約,\n カラム名2 データ型 制約,\n ...\n);\n```\n\n### 実例\n\n```sql\nCREATE TABLE employees (\n employee_id INTEGER PRIMARY KEY,\n name VARCHAR(100) NOT NULL,\n email VARCHAR(255) UNIQUE,\n hire_date DATE\n);\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "ddl-001",
+ "title": "社員テーブルを作成しよう",
+ "description": "社員情報を格納するemployeesテーブルを作成してください。employee_id(整数、主キー)、name(文字列、NOT NULL)、email(文字列、UNIQUE)、hire_date(日付)のカラムを含めてください。",
+ "difficulty": 3,
+ "hints": [
+ "CREATE TABLE文を使用します",
+ "PRIMARY KEY制約でemployee_idを主キーに設定します",
+ "NOT NULL制約でnameを必須項目に設定します",
+ "UNIQUE制約でemailの重複を防ぎます"
+ ],
+ "solution": "CREATE TABLE employees (employee_id INTEGER PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(255) UNIQUE, hire_date DATE)"
+ },
+ {
+ "type": "word-reorder",
+ "id": "ddl-002",
+ "title": "部署テーブルを作成しよう",
+ "description": "部署情報を格納するdepartmentsテーブルを作成してください。department_id(整数、主キー)、department_name(文字列、NOT NULL)、manager_id(整数)のカラムを含めてください。",
+ "difficulty": 3,
+ "hints": [
+ "CREATE TABLE文を使用します",
+ "department_idを主キーに設定します",
+ "department_nameを必須項目に設定します"
+ ],
+ "solution": "CREATE TABLE departments (department_id INTEGER PRIMARY KEY, department_name VARCHAR(100) NOT NULL, manager_id INTEGER)"
+ },
+ {
+ "type": "slide",
+ "title": "ALTER TABLEによるテーブル変更",
+ "content": "# ALTER TABLEによるテーブル変更\n\nALTER TABLE文を使用して**既存のテーブル構造を変更**できます。\n\n## 主な変更操作\n\n- **ADD COLUMN**: 新しいカラムを追加\n- **DROP COLUMN**: カラムを削除\n- **MODIFY/ALTER COLUMN**: カラムの定義を変更\n- **ADD CONSTRAINT**: 制約を追加\n- **DROP CONSTRAINT**: 制約を削除\n\n## 基本構文\n\n```sql\nALTER TABLE テーブル名 ADD COLUMN カラム名 データ型 制約;\nALTER TABLE テーブル名 DROP COLUMN カラム名;\n```\n\n### 実例\n\n```sql\nALTER TABLE employees ADD COLUMN salary DECIMAL(10,2);\nALTER TABLE employees ADD COLUMN department_id INTEGER;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "ddl-003",
+ "title": "社員テーブルにカラムを追加しよう",
+ "description": "employeesテーブルにsalary(DECIMAL(10,2))とdepartment_id(INTEGER)のカラムを追加してください。",
+ "difficulty": 4,
+ "hints": [
+ "ALTER TABLE文を使用します",
+ "ADD COLUMN句で新しいカラムを追加します",
+ "DECIMAL(10,2)は小数点以下2桁までの数値型です"
+ ],
+ "solution": "ALTER TABLE employees ADD COLUMN salary DECIMAL(10,2), ADD COLUMN department_id INTEGER"
+ },
+ {
+ "type": "slide",
+ "title": "DML(データ操作言語)の基本",
+ "content": "# DML(データ操作言語)の基本\n\nDML(Data Manipulation Language)は**データの操作**を行うためのSQL文です。\n\n## 主なDML文\n\n- **INSERT**: データの挿入\n- **UPDATE**: データの更新\n- **DELETE**: データの削除\n- **SELECT**: データの取得(既に学習済み)\n\n## INSERT文の基本構文\n\n```sql\nINSERT INTO テーブル名 (カラム1, カラム2, ...) \nVALUES (値1, 値2, ...);\n```\n\n### 実例\n\n```sql\nINSERT INTO employees (employee_id, name, email, hire_date) \nVALUES (1, '田中太郎', 'tanaka@example.com', '2023-04-01');\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "dml-001",
+ "title": "社員データを挿入しよう",
+ "description": "employeesテーブルに新しい社員データを挿入してください。employee_id=1, name='田中太郎', email='tanaka@example.com', hire_date='2023-04-01'のデータを挿入してください。",
+ "difficulty": 3,
+ "hints": [
+ "INSERT INTO文を使用します",
+ "VALUES句で挿入する値を指定します",
+ "文字列は引用符で囲みます",
+ "日付も引用符で囲みます"
+ ],
+ "solution": "INSERT INTO employees (employee_id, name, email, hire_date) VALUES (1, '田中太郎', 'tanaka@example.com', '2023-04-01')"
+ },
+ {
+ "type": "slide",
+ "title": "UPDATE文によるデータ更新",
+ "content": "# UPDATE文によるデータ更新\n\nUPDATE文を使用して**既存のデータを更新**できます。\n\n## 基本構文\n\n```sql\nUPDATE テーブル名 \nSET カラム1 = 値1, カラム2 = 値2, ...\nWHERE 条件;\n```\n\n## 重要な注意点\n\n- **WHERE句は必須**(省略すると全行が更新される)\n- 複数のカラムを同時に更新可能\n- 条件に一致する行のみが更新される\n\n### 実例\n\n```sql\nUPDATE employees \nSET salary = 500000, department_id = 1 \nWHERE employee_id = 1;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "dml-002",
+ "title": "社員の給与を更新しよう",
+ "description": "employee_id=1の社員の給与を500000に、department_idを1に更新してください。",
+ "difficulty": 4,
+ "hints": [
+ "UPDATE文を使用します",
+ "SET句で更新する値を指定します",
+ "WHERE句で更新対象を限定します",
+ "複数のカラムはカンマで区切ります"
+ ],
+ "solution": "UPDATE employees SET salary = 500000, department_id = 1 WHERE employee_id = 1"
+ },
+ {
+ "type": "slide",
+ "title": "DELETE文によるデータ削除",
+ "content": "# DELETE文によるデータ削除\n\nDELETE文を使用して**データを削除**できます。\n\n## 基本構文\n\n```sql\nDELETE FROM テーブル名 WHERE 条件;\n```\n\n## 重要な注意点\n\n- **WHERE句は必須**(省略すると全行が削除される)\n- 削除されたデータは復元できない\n- 外部キー制約がある場合は削除できない場合がある\n\n### 実例\n\n```sql\nDELETE FROM employees WHERE employee_id = 1;\n```\n\n### 全データ削除(注意!)\n\n```sql\nDELETE FROM employees; -- 全行削除\nTRUNCATE TABLE employees; -- より高速な全行削除\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "dml-003",
+ "title": "退職した社員を削除しよう",
+ "description": "employee_id=1の社員データを削除してください。",
+ "difficulty": 3,
+ "hints": [
+ "DELETE FROM文を使用します",
+ "WHERE句で削除対象を限定します",
+ "WHERE句を忘れると全データが削除されるので注意"
+ ],
+ "solution": "DELETE FROM employees WHERE employee_id = 1"
+ },
+ {
+ "type": "slide",
+ "title": "制約(Constraints)の基本",
+ "content": "# 制約(Constraints)の基本\n\n制約を使用して**データの整合性**を保つことができます。\n\n## 主な制約の種類\n\n- **PRIMARY KEY**: 主キー制約(一意性 + NOT NULL)\n- **FOREIGN KEY**: 外部キー制約(参照整合性)\n- **UNIQUE**: 一意性制約\n- **NOT NULL**: NULL値を禁止\n- **CHECK**: 値の範囲や条件をチェック\n- **DEFAULT**: デフォルト値を設定\n\n## 外部キー制約の例\n\n```sql\nALTER TABLE employees \nADD CONSTRAINT fk_department \nFOREIGN KEY (department_id) \nREFERENCES departments(department_id);\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "constraint-001",
+ "title": "外部キー制約を追加しよう",
+ "description": "employeesテーブルのdepartment_idカラムに、departmentsテーブルのdepartment_idを参照する外部キー制約を追加してください。制約名はfk_departmentとしてください。",
+ "difficulty": 5,
+ "hints": [
+ "ALTER TABLE文を使用します",
+ "ADD CONSTRAINT句で制約を追加します",
+ "FOREIGN KEY句で外部キーを定義します",
+ "REFERENCES句で参照先テーブルとカラムを指定します"
+ ],
+ "solution": "ALTER TABLE employees ADD CONSTRAINT fk_department FOREIGN KEY (department_id) REFERENCES departments(department_id)"
+ },
+ {
+ "type": "word-reorder",
+ "id": "constraint-002",
+ "title": "CHECK制約を追加しよう",
+ "description": "employeesテーブルのsalaryカラムに、給与が0以上であることをチェックするCHECK制約を追加してください。制約名はchk_salary_positiveとしてください。",
+ "difficulty": 5,
+ "hints": [
+ "ALTER TABLE文を使用します",
+ "ADD CONSTRAINT句で制約を追加します",
+ "CHECK句で条件を指定します",
+ "salary >= 0の条件を設定します"
+ ],
+ "solution": "ALTER TABLE employees ADD CONSTRAINT chk_salary_positive CHECK (salary >= 0)"
+ },
+ {
+ "type": "slide",
+ "title": "データの検証とクレンジング",
+ "content": "# データの検証とクレンジング\n\nDuckDBでは**データ品質の確保**が重要です。\n\n## データ検証の手法\n\n- **NULL値のチェック**: IS NULL / IS NOT NULL\n- **重複データの確認**: DISTINCT, GROUP BY\n- **データ型の確認**: typeof()関数\n- **範囲チェック**: BETWEEN, 比較演算子\n\n## データクレンジングの例\n\n```sql\n-- NULL値を持つ行を確認\nSELECT COUNT(*) FROM employees WHERE name IS NULL;\n\n-- 重複データを確認\nSELECT email, COUNT(*) \nFROM employees \nGROUP BY email \nHAVING COUNT(*) > 1;\n\n-- データ型を確認\nSELECT typeof(salary) FROM employees LIMIT 1;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "validation-001",
+ "title": "NULL値をチェックしよう",
+ "description": "employeesテーブルでnameカラムがNULLの行数を確認してください。",
+ "difficulty": 3,
+ "hints": [
+ "COUNT(*)で行数をカウントします",
+ "WHERE句でIS NULLを使用します",
+ "NULL値の確認は重要なデータ品質チェックです"
+ ],
+ "solution": "SELECT COUNT(*) FROM employees WHERE name IS NULL"
+ },
+ {
+ "type": "slide",
+ "title": "データのエクスポートとバックアップ",
+ "content": "# データのエクスポートとバックアップ\n\nDuckDBでは**様々な形式でデータをエクスポート**できます。\n\n## CSVエクスポート\n\n```sql\nCOPY employees TO 'backup/employees.csv' (HEADER, DELIMITER ',');\n```\n\n## Parquetエクスポート\n\n```sql\nCOPY employees TO 'backup/employees.parquet' (FORMAT PARQUET);\n```\n\n## JSONエクスポート\n\n```sql\nCOPY employees TO 'backup/employees.json' (FORMAT JSON);\n```\n\n## テーブル全体のバックアップ\n\n```sql\n-- 新しいテーブルとして保存\nCREATE TABLE employees_backup AS SELECT * FROM employees;\n```"
+ },
+ {
+ "type": "word-reorder",
+ "id": "export-001",
+ "title": "データをCSVでエクスポートしよう",
+ "description": "employeesテーブルのデータをemployees_backup.csvファイルにエクスポートしてください。ヘッダー付きで出力してください。",
+ "difficulty": 4,
+ "hints": [
+ "COPY文を使用します",
+ "TO句でファイルパスを指定します",
+ "HEADERオプションでヘッダー行を含めます",
+ "DELIMITER ','でカンマ区切りを指定します"
+ ],
+ "solution": "COPY employees TO 'employees_backup.csv' (HEADER, DELIMITER ',')"
+ }
+]
\ No newline at end of file
🎉
+
+ ${completionResult.courseTitle} を完了しました!
+
+
+