stuffAndAmountArray;
}
\ No newline at end of file
diff --git a/backend/src/main/java/com/example/todoapp/dto/StuffDetailDTO.java b/backend/src/main/java/com/example/todoapp/dto/StuffDetailDTO.java
index bf63d6d..1c70803 100644
--- a/backend/src/main/java/com/example/todoapp/dto/StuffDetailDTO.java
+++ b/backend/src/main/java/com/example/todoapp/dto/StuffDetailDTO.java
@@ -1,6 +1,7 @@
package com.example.todoapp.dto;
import lombok.Data;
+
/**
* 食材詳細情報のデータ転送オブジェクト(DTO)
*
@@ -22,6 +23,12 @@ public class StuffDetailDTO {
*/
private String stuffName;
+ /**
+ * カテゴリ名
+ * 食材のカテゴリ
+ */
+ private String category;
+
/**
* 数量
* レシピに必要な食材の量
diff --git a/backend/src/main/java/com/example/todoapp/service/RecipeService.java b/backend/src/main/java/com/example/todoapp/service/RecipeService.java
index 2eab2b8..f1a0d0d 100644
--- a/backend/src/main/java/com/example/todoapp/service/RecipeService.java
+++ b/backend/src/main/java/com/example/todoapp/service/RecipeService.java
@@ -79,13 +79,13 @@ public class RecipeService {
RecipeStuffs recipeStuffs = new RecipeStuffs();
recipeStuffs.setRecipes(recipe); // 関連レシピ設定
- recipeStuffs.setStuff(stuff); // 関連材料設定
+ recipeStuffs.setStuff(stuff); // 関連材料設定
// 数量設定、defaultは1
if (stuffDTO.getAmount() == null || stuffDTO.getAmount().isEmpty()) {
stuffDTO.setAmount("1");
}
- recipeStuffs.setAmount(Integer.parseInt(stuffDTO.getAmount()));
+ recipeStuffs.setAmount(Integer.parseInt(stuffDTO.getAmount()));
recipeStuffsList.add(recipeStuffs);
}
@@ -111,26 +111,28 @@ public class RecipeService {
*/
public RecipeDetailDTO getRecipeDetailsById(Long recipeId) {
Recipes recipe = recipesRepository.findById(recipeId)
- .orElseThrow(() -> new RuntimeException("レシピが見つかりません"));
+ .orElseThrow(() -> new RuntimeException("レシピが見つかりません"));
List recipeStuffsList = recipeStuffsRepository.findByRecipesRecipeId(recipeId);
List stuffList = recipeStuffsList.stream()
- .map(rs -> {
- StuffDetailDTO stuffDTO = new StuffDetailDTO();
- stuffDTO.setStuffId(rs.getStuff().getStuffId());
- stuffDTO.setStuffName(rs.getStuff().getStuffName());
- stuffDTO.setAmount(rs.getAmount());
- return stuffDTO;
- })
- .collect(Collectors.toList());
+ .map(rs -> {
+ StuffDetailDTO stuffDTO = new StuffDetailDTO();
+ Stuffs stuff = rs.getStuff();
+ stuffDTO.setStuffId(stuff.getStuffId());
+ stuffDTO.setStuffName(stuff.getStuffName());
+ stuffDTO.setCategory(stuff.getCategory());
+ stuffDTO.setAmount(rs.getAmount());
+ return stuffDTO;
+ })
+ .collect(Collectors.toList());
RecipeDetailDTO dto = new RecipeDetailDTO();
dto.setRecipeId(recipe.getRecipeId());
dto.setRecipeName(recipe.getRecipeName());
dto.setSummary(recipe.getSummary());
- dto.setStuffs(stuffList);
-
+ dto.setStuffAndAmountArray(stuffList);
+
return dto;
}
@@ -145,7 +147,7 @@ public class RecipeService {
public Recipes updateRecipe(RecipeDetailDTO dto) {
// IDでレシピを検索し、見つからない場合は例外をスロー
Recipes recipe = recipesRepository.findById(dto.getRecipeId())
- .orElseThrow(() -> new RuntimeException("レシピが見つかりません"));
+ .orElseThrow(() -> new RuntimeException("レシピが見つかりません"));
// レシピ名と概要を更新
recipe.setRecipeName(dto.getRecipeName());
@@ -155,12 +157,12 @@ public class RecipeService {
Set incomingStuffIds = new HashSet<>();
// 提供された材料の詳細を繰り返し処理
- for (StuffDetailDTO stuffDTO : dto.getStuffs()) {
+ for (StuffDetailDTO stuffDTO : dto.getStuffAndAmountArray()) {
if (stuffDTO.getStuffId() == null) {
// 材料IDがnullの場合、新しい材料を作成
Stuffs newStuff = new Stuffs();
newStuff.setStuffName(stuffDTO.getStuffName());
- newStuff.setCategory("その他");
+ newStuff.setCategory("その他");
newStuff = stuffsRepository.save(newStuff);
// 新しいRecipeStuffsエントリを作成
@@ -174,7 +176,7 @@ public class RecipeService {
} else {
// 材料IDが提供されている場合、既存のRecipeStuffsエントリを検索
Optional optionalRs = recipeStuffsRepository
- .findByRecipesRecipeIdAndStuffStuffId(dto.getRecipeId(), stuffDTO.getStuffId());
+ .findByRecipesRecipeIdAndStuffStuffId(dto.getRecipeId(), stuffDTO.getStuffId());
if (optionalRs.isPresent()) {
// RecipeStuffsエントリが存在する場合、数量を更新
@@ -185,7 +187,7 @@ public class RecipeService {
} else {
// オプション:見つからない場合、新しいRecipeStuffsエントリを作成
Stuffs existingStuff = stuffsRepository.findById(stuffDTO.getStuffId())
- .orElseThrow(() -> new RuntimeException("材料が見つかりません"));
+ .orElseThrow(() -> new RuntimeException("材料が見つかりません"));
RecipeStuffs rs = new RecipeStuffs();
rs.setRecipes(recipe);
@@ -207,4 +209,4 @@ public class RecipeService {
return recipe;
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 80b6945..3750af9 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -108,16 +108,16 @@ const App: React.FC = () => {
}
/>
- {/* テストページへのルートを追加 */}
+ {/* 料理リストへのルートを追加 */}
-
+
}
/>
- {/* テストページへのルートを追加 */}
+ {/* 料理追加ページ(新規追加)へのルートを追加 */}
{
}
/>
+ {/* 料理追加ページ(編集)へのルートを追加 */}
+
+
+
+ }
+ />
}>
{/* ルートパスへのアクセスはタスク一覧にリダイレクト */}
@@ -140,7 +149,7 @@ const App: React.FC = () => {
}
/>
-
+
diff --git a/frontend/src/constants/errorMessages.ts b/frontend/src/constants/errorMessages.ts
index 0aa0f98..90bcb50 100644
--- a/frontend/src/constants/errorMessages.ts
+++ b/frontend/src/constants/errorMessages.ts
@@ -41,6 +41,16 @@ export const STOCK_ERRORS = {
BUY_FAILED: '在庫リストの購入処理に失敗しました',
};
+// 料理リスト関連のエラーメッセージ
+export const RECIPE_ERRORS = {
+ FETCH_FAILED: '料理リストの取得に失敗しました',
+ GET_FAILED: '料理情報の取得に失敗しました',
+ CREATE_FAILED: '料理リストの作成に失敗しました',
+ UPDATE_FAILED: '料理リストの更新に失敗しました',
+ DELETE_FAILED: '料理リストの削除に失敗しました',
+};
+
+
// // タスク関連のエラーメッセージ
// export const TASK_ERRORS = {
// FETCH_FAILED: 'タスクの取得に失敗しました',
diff --git a/frontend/src/pages/AddRecipe.tsx b/frontend/src/pages/AddRecipe.tsx
index fe110e5..64a36ca 100644
--- a/frontend/src/pages/AddRecipe.tsx
+++ b/frontend/src/pages/AddRecipe.tsx
@@ -29,15 +29,23 @@ import {
} from '@mui/material';
import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon,
- SoupKitchen as SoupKitchenIcon, Edit as EditIcon, ListAlt as ListAltIcon
+ SoupKitchen as SoupKitchenIcon, Edit as EditIcon, Save as SaveIcon, ListAlt as ListAltIcon
} from '@mui/icons-material';
import AddStuffAmountDialog from '../components/AddStuffAmountDialog';
-import { StuffAndCategoryAndAmount, StuffNameAndAmount } from '../types/types';
+import { StuffAndCategoryAndAmount } from '../types/types';
import EditAmountDialog from '../components/EditAmountDialog';
+import { recipeApi, toBuyApi } from '../services/api';
+import { useNavigate, useParams } from 'react-router-dom';
-const AddDishes2: React.FC = () => {
- // 料理名
+const AddRecipe: React.FC = () => {
+ const { recipeId: recipeIdStr } = useParams();
+ const recipeId = recipeIdStr ? parseInt(recipeIdStr) : null
+
+ const navigate = useNavigate();
+
+ // 料理名,説明
const [recipeName, setRecipeName] = useState('');
+ const [recipeSummary, setRecipeSummary] = useState('');
// 材料リスト
const [items, setItems] = useState([]);
// 材料追加作成ダイアログの表示状態
@@ -51,105 +59,158 @@ const AddDishes2: React.FC = () => {
const [editingItem, setEditingItem] = useState(emptyItem);
const [editingItemIdx, setEditingItemIdx] = useState(0);
- const handleAddRecipeToBuy = () => {
- console.log('追加された材料:', items);
+ const loadRecipe = async () => {
+ if (recipeId && !recipeName) {
+ const recipe = await recipeApi.getById(recipeId);
+ console.log('loaded recipe=', recipe)
+ setRecipeName(recipe.recipeName)
+ setRecipeSummary(recipe.summary)
+ setItems(recipe.stuffAndAmountArray)
+ }
+ }
+ loadRecipe();
+
+ const handleSaveRecipe = async () => {
+
+ if (!recipeId) {
+ // 新規追加
+ const response = await recipeApi.addRecipe({
+ recipeName,
+ summary: recipeSummary,
+ stuffAndAmountArray: items,
+ })
+ return response.recipeId;
+ }
+
+ const response = await recipeApi.updateRecipe({
+ recipeId,
+ recipeName,
+ summary: recipeSummary,
+ stuffAndAmountArray: items,
+ })
+
+ return recipeId;
+ }
+
+ const handleSubmit = async () => {
+ const recipeId = await handleSaveRecipe()
+ alert('レシピが保存されました!') // 仮メッセージ
+ // navigate('/tasks');
+ }
+
+ const handleSubmitAndAddToBuy = async () => {
+ const recipeId = await handleSaveRecipe();
+ await toBuyApi.addByRecipe(recipeId)
+ navigate('/tasks')
}
return (
-
-
-
料理の追加
- setRecipeName(e.target.value)}
- />
-
- 材料リスト
- {/* すべての材料情報を表示 */}
- {!items.length
- ? (+ボタンで材料を追加してください
)
- : ({items.map((item, index) => (
-
-
-
- {/* 買い物リスト:食材情報記入ボタン */}
-
-
- {`× ${item.amount}`}
-
- {/* 買い物リスト:数量変更ボタン */}
-
- {
- setOpenAmountDialog(true)
- setEditingItemIdx(index)
- setEditingItem(item)
- }}
- >
-
-
-
- {/* 買い物リスト:食材削除ボタン */}
-
- setItems([...items.slice(0, index), ...items.slice(index + 1)])}>
-
-
-
-
-
-
-
-
- ))}
)}
-
-
-
-
- 材料を追加
-
-
setOpenAddDialog(true)}>
-
-
-
-
-
-
-
-
- {/* 新規材料追加ダイアログ */}
- {
- console.log('newItem:', newItem);
- setItems([...items, newItem]);
- setOpenAddDialog(false);
- }} />
-
- {/* 数量変更ダイアログ */}
- setEditingItem({ ...editingItem, ...v })}
- onSubmit={() => {
- setItems([...items.slice(0, editingItemIdx), editingItem, ...items.slice(editingItemIdx + 1)])
- setOpenAmountDialog(false);
- }} />
-
-
+ (recipeId && !recipeName)
+ ? 読み込み中...
+ :
+
+
+
+
+ {!recipeId ? '料理の追加' : '料理の編集'}
+
+ setRecipeName(e.target.value)}
+ />
+ setRecipeSummary(e.target.value)}
+ />
+
+ 材料リスト
+ {/* すべての材料情報を表示 */}
+ {(!items || !items.length)
+ ? (+ボタンで材料を追加してください
)
+ : ({items.map((item, index) => (
+
+
+
+ {/* 買い物リスト:食材情報記入ボタン */}
+
+
+ {`× ${item.amount}`}
+
+ {/* 買い物リスト:数量変更ボタン */}
+
+ {
+ setOpenAmountDialog(true)
+ setEditingItemIdx(index)
+ setEditingItem(item)
+ }}
+ >
+
+
+
+ {/* 買い物リスト:食材削除ボタン */}
+
+ setItems([...items.slice(0, index), ...items.slice(index + 1)])}>
+
+
+
+
+
+
+
+
+ ))}
)}
+
+
+
+
+ 材料を追加
+
+
setOpenAddDialog(true)}>
+
+
+
+
+
+
+
+
+
+ {/* 新規材料追加ダイアログ */}
+ {
+ console.log('newItem:', newItem);
+ setItems([...items, newItem]);
+ setOpenAddDialog(false);
+ }} />
+
+ {/* 数量変更ダイアログ */}
+ setEditingItem({ ...editingItem, ...v })}
+ onSubmit={() => {
+ setItems([...items.slice(0, editingItemIdx), editingItem, ...items.slice(editingItemIdx + 1)])
+ setOpenAmountDialog(false);
+ }} />
+
+
);
};
-export default AddDishes2;
\ No newline at end of file
+export default AddRecipe;
\ No newline at end of file
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index 2b2959b..0187ecd 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -3,8 +3,8 @@
* バックエンドAPIとの通信を担当するモジュール
* 認証、タスク管理などの機能を提供
*/
-import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, Recipes } from '../types/types';
-import { AUTH_ERRORS, TOBUY_ERRORS, STOCK_ERRORS } from '../constants/errorMessages';
+import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, RecipeData, StuffAndCategoryAndAmount } 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';
@@ -109,7 +109,7 @@ export const toBuyApi = {
* @param tobuy 作成する材料情報
* @returns 作成された材料情報
*/
- addToBuy:async (tobuy: Omit & { stuffId: number | null, category: string }): Promise => {
+ addToBuy: async (tobuy: Omit & { stuffId: number | null, category: string }): Promise => {
const response = await fetch(`${API_BASE_URL}/api/tobuy/add`, {
method: 'POST',
headers: getHeaders(),
@@ -132,6 +132,18 @@ export const toBuyApi = {
},
+ addByRecipe: async (recipeId: number): Promise => {
+ const response = await fetch(`${API_BASE_URL}/api/tobuy/addByRecipe`, {
+ method: 'POST',
+ headers: getHeaders(),
+ body: JSON.stringify({ recipeId }),
+ })
+ if (!response.ok) {
+ throw new Error(TOBUY_ERRORS.CREATE_FAILED);
+ }
+ return response.json();
+ },
+
/**
* 買うものリストを削除
* @param id 削除対象の買うものリストID
@@ -159,7 +171,7 @@ export const toBuyApi = {
* @param tobuy 変更する材料情報
* @returns 変更された材料情報
*/
- updateToBuy: async (tobuy:ToBuy): Promise => {
+ updateToBuy: async (tobuy: ToBuy): Promise => {
const response = await fetch(`${API_BASE_URL}/api/tobuy/update`, {
method: 'PUT',
headers: getHeaders(),
@@ -274,7 +286,7 @@ export const stockApi = {
req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || ''
req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || ''
- console.log('req: ', req)
+ console.log('req: ', req)
const response = await fetch(`${API_BASE_URL}/api/stocks/update`, {
method: 'PUT',
@@ -331,11 +343,13 @@ export const recipeApi = {
* @param recipeData レシピデータ(名前、説明、材料リスト)
* @returns レシピ追加レスポンス(内联类型)
*/
- addRecipe: async (recipeData: Recipes): Promise<{
+ addRecipe: async (recipeData: RecipeData): Promise<{
result: string;
- recipe_id: number;
+ recipeId: number;
message: string;
}> => {
+ console.log('recipeData:', recipeData)
+
const response = await fetch(`${API_BASE_URL}/api/recipes/add`, {
method: 'POST',
headers: getHeaders(), // 認証トークンを含むヘッダー
@@ -344,7 +358,31 @@ export const recipeApi = {
if (!response.ok) {
const errorData = await response.json().catch(() => null);
- throw new Error(errorData?.message);
+ 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 } & RecipeData): 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();
@@ -382,7 +420,7 @@ export const recipeApi = {
recipeId: number;
recipeName: string;
summary: string;
- stuffs: Array<{ stuffId: number; stuffName: string; amount: number }>;
+ stuffAndAmountArray: StuffAndCategoryAndAmount[];
}> => {
const response = await fetch(`${API_BASE_URL}/api/recipes/getById?recipeId=${recipeId}`, {
method: 'GET',
@@ -396,26 +434,6 @@ export const recipeApi = {
return response.json();
},
-
- /**
- * レシピを更新
- * @param recipeData 更新するレシピ情報
- * @returns 更新結果(成功/失敗)
- */
- update: async (recipeData: { recipeId: number } & Recipes): Promise<{ result: boolean; message: string }> => {
- const response = await fetch(`${API_BASE_URL}/api/recipes/update`, {
- method: 'POST',
- headers: getHeaders(),
- body: JSON.stringify(recipeData),
- });
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => null);
- throw new Error(errorData?.message);
- }
-
- return response.json();
- },
};
diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts
index 287d839..190c1f7 100644
--- a/frontend/src/types/types.ts
+++ b/frontend/src/types/types.ts
@@ -123,16 +123,16 @@ export interface RegisterCredentials {
* レシピ登録用の複合型定義
* レシピ基本情報と材料リストを統合
*/
-export interface Recipes {
+export interface RecipeData {
recipeName: string;// レシピ名
summary: string;// レシピ概要
// 材料リスト(直接配列として内包)
- stuffAndAmountArray: Array<{
+ stuffAndAmountArray: {
// 既存材料IDまたは新規作成情報のどちらか一方のみ必要
- stuffId?: number; // 既存材料ID(オプション)
+ stuffId: number | null; // 既存材料ID(オプション)
stuffName?: string;// 新規材料名(オプション)
category?: string; // 新規材料カテゴリ(オプション)
- amount?: number; // 使用量(必須)
- }>;
+ amount: number; // 使用量(必須)
+ }[];
}
\ No newline at end of file