// --- APIクライアント --- const ApiClient = { /** * 共通のAPI呼び出し処理(FastAPI REST形式対応) * @param {string} endpoint - APIエンドポイント(例: "/api/register_user") * @param {Object} requestBody - リクエストボディ(actionは不要) * @param {string} operationName - 操作名(ログ用) * @returns {Promise} APIレスポンス */ async _callApi(endpoint, requestBody, operationName) { console.log(`[ApiClient] ${operationName} - Request to ${endpoint}:`, requestBody); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.TIMEOUT); const response = await fetch(API_CONFIG.BASE_URL + endpoint, { method: 'POST', mode: 'cors', credentials: 'omit', headers: { 'Content-Type': 'application/json' // FastAPI標準: application/json }, body: JSON.stringify(requestBody), signal: controller.signal }); clearTimeout(timeoutId); // HTTPステータスチェック if (!response.ok) { console.error(`[ApiClient] ${operationName} - HTTP Error:`, response.status, response.statusText); return { success: false, error: { code: 'HTTP_ERROR', message: `HTTPエラー: ${response.status} ${response.statusText}` } }; } const result = await response.json(); console.log(`[ApiClient] ${operationName} - Response:`, result); // レスポンス形式チェック if (typeof result !== 'object' || result === null) { console.error(`[ApiClient] ${operationName} - Invalid response format:`, result); return { success: false, error: { code: 'INVALID_RESPONSE', message: 'APIからの応答形式が不正です' } }; } // FastAPI形式: {success: true, data: {...}} はそのまま返す if (result.hasOwnProperty('success')) { return result; } // GAS形式: {status: "success"} → {success: true} に変換(後方互換性) if (result.status === 'success') { return { success: true, data: result.data || {}, message: result.message }; } else if (result.status === 'error') { return { success: false, error: { code: result.error?.code || 'API_ERROR', message: result.error?.message || result.message || 'APIエラーが発生しました' } }; } // その他の形式はそのまま返す(モックデータなど) return result; } catch (error) { console.error(`[ApiClient] ${operationName} - Error:`, error); // エラータイプ別のメッセージ let errorMessage = 'APIとの通信に失敗しました'; let errorCode = 'NETWORK_ERROR'; if (error.name === 'AbortError') { errorMessage = '通信がタイムアウトしました。もう一度お試しください。'; errorCode = 'TIMEOUT'; } else if (error instanceof TypeError) { errorMessage = 'ネットワーク接続を確認してください'; errorCode = 'NETWORK_ERROR'; } return { success: false, error: { code: errorCode, message: errorMessage, details: error.message } }; } }, /** * ユーザー登録(新規のみ) * @param {string} username - ユーザー名 * @param {string} password - パスワード * @param {string} inviteCode - 招待コード * @returns {Promise} { success, data: { user_id, username, created_at } } */ async registerUser(username, password, inviteCode) { if (API_CONFIG.USE_MOCK) { return MockData.registerUser(username); } return await this._callApi( '/api/register_user', { username, password, invite_code: inviteCode }, 'registerUser' ); }, /** * ログイン * @param {string} username - ユーザー名 * @param {string} password - パスワード * @returns {Promise} { success, data: { user_id, username } } */ async login(username, password) { if (API_CONFIG.USE_MOCK) { return MockData.registerUser(username); // モックは既存を流用 } return await this._callApi( '/api/login', { username, password }, 'login' ); }, /** * セッション開始 * @param {string} userId - ユーザーID * @param {string[]} subjects - 選択科目リスト * @returns {Promise} { success, data: { session_id, start_time } } */ async startSession(userId, subjects) { if (API_CONFIG.USE_MOCK) { return MockData.startSession(userId, subjects); } return await this._callApi( '/api/start_session', { user_id: userId, subjects }, 'startSession' ); }, /** * 問題生成 * @param {string} sessionId - セッションID * @param {string[]} subjects - 選択科目リスト * @returns {Promise} { success, data: { session_id, questions, total_count } } */ async generateQuestions(sessionId, subjects, userId = null) { if (API_CONFIG.USE_MOCK) { return MockData.generateQuestions(sessionId, subjects); } const payload = { session_id: sessionId, subjects }; // v1.4.0: user_idを追加(ジャンルバランス・除外キーワード用) if (userId) { payload.user_id = userId; } return await this._callApi( '/api/generate_questions', payload, 'generateQuestions' ); }, /** * 解答送信 * @param {string} sessionId - セッションID * @param {Array} answers - 解答リスト * @returns {Promise} { success, data: { correct_count, total_count, score, details } } */ async submitAnswers(sessionId, answers) { if (API_CONFIG.USE_MOCK) { return MockData.submitAnswers(sessionId, answers); } // API仕様に合わせて変換(FastAPI + GAS APIが期待する形式) // 重要: GAS側で === 比較するため、user_answerとcorrect_answerの型を統一 const formattedAnswers = answers.map(ans => { const correctAns = ans.correct_answer ?? ans.correct; return { question_id: ans.question_id, selected_answer: parseInt(ans.selected_choice, 10), // FastAPIスキーマ要求(int型) user_answer: parseInt(ans.selected_choice, 10), // GAS比較用(数値型) correct_answer: parseInt(correctAns, 10), // GAS比較用(数値型で統一) subject: ans.subject || '', category: ans.category || '', time_taken_seconds: ans.time_taken_seconds || 0 }; }); const result = await this._callApi( '/api/submit_answers', { session_id: sessionId, answers: formattedAnswers }, 'submitAnswers' ); console.log('[apiClient.submitAnswers] API response:', result); console.log('[apiClient.submitAnswers] result.data:', result?.data); return result; }, /** * 統計取得 * @param {string} userId - ユーザーID * @param {string[]} subjects - 対象科目(オプション) * @returns {Promise} { success, data: { subjects, radar_chart } } */ async getStatistics(userId, subjects = null) { if (API_CONFIG.USE_MOCK) { return MockData.getStatistics(userId); } const requestBody = { user_id: userId }; if (subjects && subjects.length > 0) { requestBody.subjects = subjects; } return await this._callApi( '/api/get_statistics', requestBody, 'getStatistics' ); }, /** * 評価取得 * @param {string} sessionId - セッションID * @returns {Promise} { success, data: { evaluations, overall } } */ async getEvaluation(sessionId) { if (API_CONFIG.USE_MOCK) { return MockData.getEvaluation(sessionId); } return await this._callApi( '/api/get_evaluation', { session_id: sessionId }, 'getEvaluation' ); } };