Spaces:
Sleeping
Sleeping
File size: 9,414 Bytes
994a408 2e8efb0 994a408 2e8efb0 994a408 2e8efb0 994a408 2e8efb0 994a408 2e8efb0 994a408 2e10633 994a408 2e10633 994a408 2e10633 994a408 fd41c82 4044108 994a408 3f841bc 994a408 3f841bc 994a408 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | // --- 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'
);
}
};
|