Spaces:
Sleeping
Sleeping
| // --- APIクライアント --- | |
| const ApiClient = { | |
| /** | |
| * 共通のAPI呼び出し処理(FastAPI REST形式対応) | |
| * @param {string} endpoint - APIエンドポイント(例: "/api/register_user") | |
| * @param {Object} requestBody - リクエストボディ(actionは不要) | |
| * @param {string} operationName - 操作名(ログ用) | |
| * @returns {Promise<Object>} 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<Object>} { 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<Object>} { 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<Object>} { 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<Object>} { 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<Object>} { 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<Object>} { 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<Object>} { 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' | |
| ); | |
| } | |
| }; | |