/** * APIサービス * バックエンドAPIとの通信を担当するモジュール * 認証、タスク管理などの機能を提供 */ import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, RecipeDetail, StuffAndCategoryAndAmount, RecipeWithId, StockHistory, StockUpdateRequest } from '../types/types'; import { AUTH_ERRORS, TOBUY_ERRORS, STOCK_ERRORS, RECIPE_ERRORS } from '../constants/errorMessages'; // APIのベースURL - 環境変数から取得するか、デフォルト値を使用 const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080'; /** * APIリクエスト用のヘッダーを生成する関数 * @param includeAuth 認証トークンをヘッダーに含めるかどうか * @returns リクエストヘッダーオブジェクト */ const getHeaders = (includeAuth: boolean = true) => { const headers: Record = { 'Content-Type': 'application/json', }; // 認証トークンが必要な場合はローカルストレージから取得してヘッダーに追加 if (includeAuth) { const token = localStorage.getItem('token'); if (token) { headers['Authorization'] = `Bearer ${token}`; } } return headers; }; /** * 認証関連のAPI機能を提供するオブジェクト * ログインと新規ユーザー登録の機能を含む */ export const authApi = { /** * ユーザーログイン処理 * @param credentials ログイン情報(ユーザー名とパスワード) * @returns 認証レスポンス(トークンとユーザー情報) */ login: async (credentials: LoginCredentials): Promise => { const response = await fetch(`${API_BASE_URL}/api/auth/login`, { method: 'POST', headers: getHeaders(false), // 認証前なのでトークンは不要 body: JSON.stringify(credentials), }); // エラーレスポンスの処理 if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error( errorData?.message || AUTH_ERRORS.LOGIN_FAILED ); } return response.json(); }, /** * 新規ユーザー登録処理 * @param credentials 登録情報(ユーザー名、パスワード) * @returns 認証レスポンス(トークンとユーザー情報) */ register: async (credentials: RegisterCredentials): Promise => { const response = await fetch(`${API_BASE_URL}/api/auth/register`, { method: 'POST', headers: getHeaders(false), // 認証前なのでトークンは不要 body: JSON.stringify(credentials), }); // エラーレスポンスの処理 if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error( errorData?.message || AUTH_ERRORS.REGISTER_FAILED ); } return response.json(); }, }; /** * 買うものリスト管理関連のAPI機能を提供するオブジェクト * 買うものリストの取得、作成、更新、削除などの機能を含む */ export const toBuyApi = { /** * 全買うものリストを取得 * @returns 買うものリスト一覧 */ getToBuys: async (): Promise => { const response = await fetch(`${API_BASE_URL}/api/tobuy/get`, { headers: getHeaders(), // 認証トークンを含むヘッダー }); if (!response.ok) { throw new Error(TOBUY_ERRORS.FETCH_FAILED); } return await response.json(); }, /** * 買うものリストへの材料追加 * @param tobuy 作成する材料情報 * @returns 作成された材料情報 */ addToBuy: async (tobuy: Omit & { stuffId: number | null, category: string }): Promise => { const response = await fetch(`${API_BASE_URL}/api/tobuy/add`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(tobuy), }); if (!response.ok) { throw new Error(TOBUY_ERRORS.CREATE_FAILED); } return response.json(); // return {result: true} // return { // "result": true, // "tobuyId": 1, // "stuffId": 6, // "message": "追加に成功しました", // } }, addByRecipe: async (recipeId: number, servings: number, difference: boolean): Promise => { const response = await fetch(`${API_BASE_URL}/api/tobuy/addByRecipe`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ recipeId, servings, difference }), }) if (!response.ok) { throw new Error(TOBUY_ERRORS.CREATE_FAILED); } return response.json(); }, /** * 買うものリストを削除 * @param id 削除対象の買うものリストID */ deleteToBuy: async (tobuyId: number): Promise<{ result: boolean }> => { const response = await fetch(`${API_BASE_URL}/api/tobuy/delete`, { method: 'DELETE', headers: getHeaders(), body: JSON.stringify({ tobuyId }), }); if (!response.ok) { throw new Error(TOBUY_ERRORS.DELETE_FAILED); } return response.json() // return { // "result": true // } }, /** * 買うものリストの変更 * @param tobuy 変更する材料情報 * @returns 変更された材料情報 */ updateToBuy: async (tobuy: ToBuy): Promise => { const response = await fetch(`${API_BASE_URL}/api/tobuy/update`, { method: 'PUT', headers: getHeaders(), body: JSON.stringify(tobuy), }); if (!response.ok) { throw new Error(TOBUY_ERRORS.CREATE_FAILED); } return response.json(); }, /** * 買うものリストの在庫登録(購入処理) */ buy: async (req: { tobuyId: number, amount: number, price: number, shop: string, expDate: string, buyDate: string, lastUpdate: string }): Promise<{ result: boolean }> => { console.log('/api/tobuy/buy request: ', req) req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || '' req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || '' const response = await fetch(`${API_BASE_URL}/api/tobuy/buy`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(req), }); if (!response.ok) { throw new Error(TOBUY_ERRORS.BUY_FAILED); } return response.json() }, } export const stuffApi = { getStuffs: async (category: string): Promise => { const data = await fetch(`${API_BASE_URL}/api/stuff/get?category=${encodeURIComponent(category)}`, { headers: getHeaders(), // 認証トークンを含むヘッダー }); if (!data.ok) { throw new Error(`Failed to fetch stuffs for category ${category}`); } return data.json(); // const data = [ // { stuffId: 1, stuffName: "牛乳", category: "乳製品" }, // { stuffId: 2, stuffName: "ヨーグルト", category: "乳製品" }, // { stuffId: 3, stuffName: "チーズ", category: "乳製品" }, // { stuffId: 4, stuffName: "バター", category: "乳製品" }, // { stuffId: 5, stuffName: "生クリーム", category: "乳製品" }, // { stuffId: 6, stuffName: "鮭", category: "魚・肉" }, // { stuffId: 7, stuffName: "鶏むね肉", category: "魚・肉" }, // { stuffId: 8, stuffName: "豚バラ肉", category: "魚・肉" }, // { stuffId: 9, stuffName: "牛ひき肉", category: "魚・肉" }, // { stuffId: 10, stuffName: "まぐろ", category: "魚・肉" }, // { stuffId: 11, stuffName: "にんじん", category: "野菜" }, // { stuffId: 12, stuffName: "キャベツ", category: "野菜" }, // { stuffId: 13, stuffName: "ほうれん草", category: "野菜" }, // { stuffId: 14, stuffName: "玉ねぎ", category: "野菜" }, // { stuffId: 15, stuffName: "ピーマン", category: "野菜" }, // { stuffId: 16, stuffName: "醤油", category: "調味料" }, // { stuffId: 17, stuffName: "味噌", category: "調味料" }, // { stuffId: 18, stuffName: "塩", category: "調味料" }, // { stuffId: 19, stuffName: "砂糖", category: "調味料" }, // { stuffId: 20, stuffName: "酢", category: "調味料" }, // { stuffId: 21, stuffName: "米", category: "その他" }, // { stuffId: 22, stuffName: "パスタ", category: "その他" }, // { stuffId: 23, stuffName: "小麦粉", category: "その他" }, // { stuffId: 24, stuffName: "卵", category: "その他" }, // { stuffId: 25, stuffName: "豆腐", category: "その他" } // ] // const filtered = data.filter(stuff => stuff.category == category) // return filtered } } export const stockApi = { /** * 全在庫リストを取得 * @returns 買在庫リスト一覧 */ getStocks: async (): Promise => { const response = await fetch(`${API_BASE_URL}/api/stocks/get`, { headers: getHeaders(), // 認証トークンを含むヘッダー }); if (!response.ok) { throw new Error(STOCK_ERRORS.FETCH_FAILED); } return response.json(); }, /** * 在庫リストへの新規食材追加 * @param stock 作成する材料情報 * @returns 作成された材料情報 */ addStock: async (stock: Omit & { stuffId: number | null }): Promise<{ result: boolean; message: string }> => { console.log("送信するデータ:", stock); // 送信前のデータ確認 stock.buyDate = makeDateObject(stock.buyDate)?.toISOString()?.substring(0, 10) || '' stock.expDate = makeDateObject(stock.expDate)?.toISOString()?.substring(0, 10) || '' console.log("変換後のデータ:", stock); // 日付変換後のデータ確認 const response = await fetch(`${API_BASE_URL}/api/stocks/add`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(stock), }); console.log("レスポンスステータス:", response.status); console.log("レスポンスヘッダー:", response.headers); // console.log("レスポンス内容:", await response.text()); if (!response.ok) { throw new Error(STOCK_ERRORS.CREATE_FAILED); } return response.json(); // return {result: true} }, /** * 在庫リストの編集 */ updateStock: async (req: StockUpdateRequest): Promise<{ result: boolean; message: string }> => { // console.log('req: ', req) req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || '' req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || '' console.log('req: ', req) const response = await fetch(`${API_BASE_URL}/api/stocks/update`, { method: 'PUT', headers: getHeaders(), body: JSON.stringify(req), }); if (!response.ok) { throw new Error(STOCK_ERRORS.UPDATE_FAILED); } return response.json() // return { // "result": true // } }, /** * 在庫リストの削除 * @param id 削除対象の食材のID */ deleteStock: async (stockId: number): Promise<{ result: boolean; message: string }> => { const response = await fetch(`${API_BASE_URL}/api/stocks/delete`, { method: 'DELETE', headers: getHeaders(), body: JSON.stringify({ stockId }), }); console.log("API レスポンスステータス:", response.status); // console.log("API レスポンス本文:", await response.text()); if (!response.ok) { throw new Error(STOCK_ERRORS.DELETE_FAILED); } const response_json = response.json() console.log('Delete response:', response_json) return response_json // return { // "result": true // } }, /** * 指定した材料の履歴を取得 * @param recipeId 取得対象の材料ID * @returns 材料の履歴の配列 */ getHistories: async (stuffId: number): Promise => { const response = await fetch(`${API_BASE_URL}/api/stocks/getHistory?stuffId=${stuffId}`, { method: 'GET', headers: getHeaders(), }); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error(errorData?.message); } return response.json(); }, }; /** * レシピ管理関連のAPI機能を提供するオブジェクト */ export const recipeApi = { /** * 新規レシピ追加処理 * @param recipeData レシピデータ(名前、説明、材料リスト) * @returns レシピ追加レスポンス(内联类型) */ addRecipe: async (recipeData: RecipeDetail): Promise<{ result: string; recipeId: number; message: string; }> => { console.log('recipeData:', recipeData) const response = await fetch(`${API_BASE_URL}/api/recipes/add`, { method: 'POST', headers: getHeaders(), // 認証トークンを含むヘッダー body: JSON.stringify(recipeData), }); if (!response.ok) { const errorData = await response.json().catch(() => null); console.error('/api/recipes/add failed on backend:', errorData?.message) throw new Error(RECIPE_ERRORS.CREATE_FAILED); } return response.json(); }, /** * レシピを更新 * @param recipeData 更新するレシピ情報 * @returns 更新結果(成功/失敗) */ updateRecipe: async (recipeData: { recipeId: number } & RecipeDetail): Promise<{ result: boolean; message: string }> => { // console.log('recipeData:', recipeData) const response = await fetch(`${API_BASE_URL}/api/recipes/update`, { method: 'PUT', headers: getHeaders(), body: JSON.stringify(recipeData), }); if (!response.ok) { const errorData = await response.json().catch(() => null); console.error('/api/recipes/update failed on backend:', errorData?.message) throw new Error(RECIPE_ERRORS.UPDATE_FAILED); } return response.json(); }, /** * 全レシピを取得 * @returns レシピ一覧 */ getAllRecipes: async (): Promise => { const response = await fetch(`${API_BASE_URL}/api/recipes/getAll`, { method: 'GET', headers: getHeaders(), // 認証トークンを含むヘッダー }); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error( errorData?.message ); } return response.json(); }, /** * レシピ詳細を取得 * @param recipeId 取得対象のレシピID * @returns レシピ詳細情報 */ getById: async (recipeId: number): Promise<{ recipeId: number; recipeName: string; summary: string; stuffAndAmountArray: StuffAndCategoryAndAmount[]; }> => { const response = await fetch(`${API_BASE_URL}/api/recipes/getById?recipeId=${recipeId}`, { method: 'GET', headers: getHeaders(), }); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error(errorData?.message); } return response.json(); }, }; function makeDateObject(dateStr: String) { // 例: '2025/06/15' または '2025-06-15' を '2025-06-15' に変換 const parts = dateStr.split(/[-\/]/); // ハイフンかスラッシュで分割 if (parts.length === 3) { return new Date(parts[0] + '-' + parts[1] + '-' + parts[2]); } return null; // 無効な日付の場合 } // /** // * (サンプル,実際には不要) // * タスク管理関連のAPI機能を提供するオブジェクト // * タスクの取得、作成、更新、削除などの機能を含む // */ // export const taskApi = { // /** // * 全タスクを取得 // * @returns タスク一覧 // */ // getTasks: async (): Promise => { // const response = await fetch(`${API_BASE_URL}/api/tasks`, { // headers: getHeaders(), // 認証トークンを含むヘッダー // }); // if (!response.ok) { // throw new Error(TASK_ERRORS.FETCH_FAILED); // } // return response.json(); // }, // /** // * 指定IDのタスクを取得 // * @param id タスクID // * @returns 単一のタスク情報 // */ // getTask: async (id: number): Promise => { // const response = await fetch(`${API_BASE_URL}/api/tasks/${id}`, { // headers: getHeaders(), // }); // if (!response.ok) { // throw new Error(TASK_ERRORS.FETCH_FAILED); // } // return response.json(); // }, // /** // * 新規材料を作成 // * @param task 作成するタスク情報(価格,作成日時、更新日時は除外) // * @returns 作成されたタスク情報 // */ // addStuff: async (task: Omit): Promise => { // const response = await fetch(`${API_BASE_URL}/api/tubuy/add`, { // method: 'POST', // headers: getHeaders(), // body: JSON.stringify(task), // }); // if (!response.ok) { // throw new Error(TASK_ERRORS.CREATE_FAILED); // } // return response.json(); // }, // /** // * タスクを更新 // * @param id 更新対象のタスクID // * @param task 更新するタスク情報(部分的な更新も可能) // * @returns 更新後のタスク情報 // */ // updateTask: async (id: number, task: Partial): Promise => { // const response = await fetch(`${API_BASE_URL}/api/tasks/${id}`, { // method: 'PUT', // headers: getHeaders(), // body: JSON.stringify(task), // }); // if (!response.ok) { // throw new Error(TASK_ERRORS.UPDATE_FAILED); // } // return response.json(); // }, // /** // * タスクを削除 // * @param id 削除対象のタスクID // */ // deleteTask: async (id: number): Promise => { // const response = await fetch(`${API_BASE_URL}/api/tasks/${id}`, { // method: 'DELETE', // headers: getHeaders(), // }); // if (!response.ok) { // throw new Error(TASK_ERRORS.DELETE_FAILED); // } // }, // };