料理追加・編集画面を実装

feature-frontend-dishedit-kato
Masaharu.Kato 4 months ago
parent 8fd8771ac5
commit 75b055574f
  1. 2
      backend/src/main/java/com/example/todoapp/dto/RecipeDetailDTO.java
  2. 7
      backend/src/main/java/com/example/todoapp/dto/StuffDetailDTO.java
  3. 10
      backend/src/main/java/com/example/todoapp/service/RecipeService.java
  4. 13
      frontend/src/App.tsx
  5. 10
      frontend/src/constants/errorMessages.ts
  6. 85
      frontend/src/pages/AddRecipe.tsx
  7. 70
      frontend/src/services/api.ts
  8. 10
      frontend/src/types/types.ts

@ -35,5 +35,5 @@ public class RecipeDetailDTO {
* 食材リスト
* このレシピに必要な食材とその数量のリスト
*/
private List<StuffDetailDTO> stuffs;
private List<StuffDetailDTO> stuffAndAmountArray;
}

@ -1,6 +1,7 @@
package com.example.todoapp.dto;
import lombok.Data;
/**
* 食材詳細情報のデータ転送オブジェクトDTO
* <p>
@ -22,6 +23,12 @@ public class StuffDetailDTO {
*/
private String stuffName;
/**
* カテゴリ名
* 食材のカテゴリ
*/
private String category;
/**
* 数量
* レシピに必要な食材の量

@ -118,8 +118,10 @@ public class RecipeService {
List<StuffDetailDTO> stuffList = recipeStuffsList.stream()
.map(rs -> {
StuffDetailDTO stuffDTO = new StuffDetailDTO();
stuffDTO.setStuffId(rs.getStuff().getStuffId());
stuffDTO.setStuffName(rs.getStuff().getStuffName());
Stuffs stuff = rs.getStuff();
stuffDTO.setStuffId(stuff.getStuffId());
stuffDTO.setStuffName(stuff.getStuffName());
stuffDTO.setCategory(stuff.getCategory());
stuffDTO.setAmount(rs.getAmount());
return stuffDTO;
})
@ -129,7 +131,7 @@ public class RecipeService {
dto.setRecipeId(recipe.getRecipeId());
dto.setRecipeName(recipe.getRecipeName());
dto.setSummary(recipe.getSummary());
dto.setStuffs(stuffList);
dto.setStuffAndAmountArray(stuffList);
return dto;
}
@ -155,7 +157,7 @@ public class RecipeService {
Set<Long> incomingStuffIds = new HashSet<>();
// 提供された材料の詳細を繰り返し処理
for (StuffDetailDTO stuffDTO : dto.getStuffs()) {
for (StuffDetailDTO stuffDTO : dto.getStuffAndAmountArray()) {
if (stuffDTO.getStuffId() == null) {
// 材料IDがnullの場合、新しい材料を作成
Stuffs newStuff = new Stuffs();

@ -108,7 +108,7 @@ const App: React.FC = () => {
</PrivateRoute>
}
/>
{/* テストページへのルートを追加 */}
{/* 料理リストへのルートを追加 */}
<Route
path="dishList"
element={
@ -117,7 +117,7 @@ const App: React.FC = () => {
</PrivateRoute>
}
/>
{/* テストページへのルートを追加 */}
{/* 料理追加ページ(新規追加)へのルートを追加 */}
<Route
path="addRecipe"
element={
@ -126,6 +126,15 @@ const App: React.FC = () => {
</PrivateRoute>
}
/>
{/* 料理追加ページ(編集)へのルートを追加 */}
<Route
path="addRecipe/:recipeId"
element={
<PrivateRoute>
<AddRecipe />
</PrivateRoute>
}
/>
</Route>
<Route path="/" element={<Layout />}>
{/* ルートパスへのアクセスはタスク一覧にリダイレクト */}

@ -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: 'タスクの取得に失敗しました',

@ -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<string>('');
const [recipeSummary, setRecipeSummary] = useState<string>('');
// 材料リスト
const [items, setItems] = useState<StuffAndCategoryAndAmount[]>([]);
// 材料追加作成ダイアログの表示状態
@ -51,21 +59,71 @@ const AddDishes2: React.FC = () => {
const [editingItem, setEditingItem] = useState<StuffAndCategoryAndAmount>(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 (
(recipeId && !recipeName)
? <p>...</p>
:
<Box>
<div>
<h1></h1>
<h1>
<SoupKitchenIcon sx={{ marginRight: "0.5em" }} />
{!recipeId ? '料理の追加' : '料理の編集'}
</h1>
<TextField autoFocus margin="dense" label="料理名" fullWidth
value={recipeName} onChange={(e) => setRecipeName(e.target.value)}
/>
<TextField autoFocus margin="dense" label="説明" fullWidth
value={recipeSummary} onChange={(e) => setRecipeSummary(e.target.value)}
/>
</div>
<h2></h2>
{/* すべての材料情報を表示 */}
{!items.length
{(!items || !items.length)
? (<p>+</p>)
: (<List>{items.map((item, index) => (
@ -124,10 +182,13 @@ const AddDishes2: React.FC = () => {
</div>
<div style={{ position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "2%" }}>
<Button variant='contained' sx={{ fontSize: "1.0rem" }}
color="primary" onClick={handleAddRecipeToBuy}>
<Button variant='contained' color="primary" onClick={handleSubmit} sx={{ marginRight: "1rem" }}>
<SaveIcon sx={{ fontSize: "1.5rem", marginRight: "0.5rem" }} />
</Button>
<Button variant='contained' color="primary" onClick={handleSubmitAndAddToBuy}>
<ListAltIcon sx={{ fontSize: "1.5rem", marginRight: "0.5rem" }} />
</Button>
</div>
@ -152,4 +213,4 @@ const AddDishes2: React.FC = () => {
);
};
export default AddDishes2;
export default AddRecipe;

@ -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';
@ -132,6 +132,18 @@ export const toBuyApi = {
},
addByRecipe: async (recipeId: number): Promise<any> => {
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
@ -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();
},
};

@ -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; // 使用量(必須)
}[];
}
Loading…
Cancel
Save