ChoTensai_V3 / static /js /apiClient.js
TOMOCHIN4
debug: SCORE 0/0問題のデバッグロギング追加 (v1.6.12)
3f841bc
// --- 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'
);
}
};