From 75b055574f1e76446afc95920648058d554e871e Mon Sep 17 00:00:00 2001 From: "Masaharu.Kato" Date: Thu, 12 Jun 2025 14:58:09 +0900 Subject: [PATCH] =?UTF-8?q?=E6=96=99=E7=90=86=E8=BF=BD=E5=8A=A0=E3=83=BB?= =?UTF-8?q?=E7=B7=A8=E9=9B=86=E7=94=BB=E9=9D=A2=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/todoapp/dto/RecipeDetailDTO.java | 4 +- .../example/todoapp/dto/StuffDetailDTO.java | 7 + .../todoapp/service/RecipeService.java | 40 +-- frontend/src/App.tsx | 17 +- frontend/src/constants/errorMessages.ts | 10 + frontend/src/pages/AddRecipe.tsx | 261 +++++++++++------- frontend/src/services/api.ts | 76 +++-- frontend/src/types/types.ts | 10 +- 8 files changed, 266 insertions(+), 159 deletions(-) diff --git a/backend/src/main/java/com/example/todoapp/dto/RecipeDetailDTO.java b/backend/src/main/java/com/example/todoapp/dto/RecipeDetailDTO.java index 9702ebd..bc45ee4 100644 --- a/backend/src/main/java/com/example/todoapp/dto/RecipeDetailDTO.java +++ b/backend/src/main/java/com/example/todoapp/dto/RecipeDetailDTO.java @@ -12,7 +12,7 @@ import lombok.Data; *

*/ @Data -public class RecipeDetailDTO { +public class RecipeDetailDTO { /** * レシピID * ユニークなレシピを識別するためのID @@ -35,5 +35,5 @@ public class RecipeDetailDTO { * 食材リスト * このレシピに必要な食材とその数量のリスト */ - private List stuffs; + private List 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