You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
joint_exc/frontend/src/services/api.ts

590 lines
18 KiB

/**
* 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<string, string> = {
'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<AuthResponse> => {
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<AuthResponse> => {
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<ToBuy[]> => {
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<ToBuy, 'stuffId' | 'tobuyId'> & { stuffId: number | null, category: string }): Promise<any> => {
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<any> => {
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<any> => {
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<Stuff[]> => {
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<Stock[]> => {
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<Stock, 'stockId' | 'stuffId'> & { 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<StockHistory[]> => {
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<RecipeWithId[]> => {
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<Task[]> => {
// 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<Task> => {
// 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<Task, 'userId' | 'createdAt' | 'price' | 'buyDate' | 'expirationDate' | 'newAddition'>): Promise<Task> => {
// 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<Task>): Promise<Task> => {
// 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<void> => {
// const response = await fetch(`${API_BASE_URL}/api/tasks/${id}`, {
// method: 'DELETE',
// headers: getHeaders(),
// });
// if (!response.ok) {
// throw new Error(TASK_ERRORS.DELETE_FAILED);
// }
// },
// };