Compare commits

...

6 Commits

  1. 9
      backend/src/main/java/com/example/todoapp/controller/RecipesController.java
  2. 6
      backend/src/main/java/com/example/todoapp/dto/RecipeDetailDTO.java
  3. 4
      backend/src/main/java/com/example/todoapp/service/RecipeService.java
  4. 124
      frontend/src/components/AddByRecipeDialog.tsx
  5. 90
      frontend/src/pages/RecipeList.tsx
  6. 9
      frontend/src/services/api.ts
  7. 17
      frontend/src/types/types.ts

@ -99,8 +99,12 @@ public class RecipesController {
public ResponseEntity<RecipeDetailDTO> getRecipeById( public ResponseEntity<RecipeDetailDTO> getRecipeById(
Authentication authentication, Authentication authentication,
@RequestParam Long recipeId) { @RequestParam Long recipeId) {
recipeService.getRecipeDetailsById(recipeId);
return ResponseEntity.ok(recipeService.getRecipeDetailsById(recipeId)); User user = userRepository.findByUsername(authentication.getName())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
RecipeDetailDTO response = recipeService.getRecipeDetailsById(user.getId(), recipeId);
return ResponseEntity.ok(response);
} }
/** /**
@ -120,7 +124,6 @@ public class RecipesController {
} else { } else {
response.put("result", false); response.put("result", false);
response.put("message", "編集に失敗しました"); response.put("message", "編集に失敗しました");
} }
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }

@ -30,6 +30,12 @@ public class RecipeDetailDTO {
* レシピの簡単な説明文 * レシピの簡単な説明文
*/ */
private String summary; private String summary;
/**
* 最大調理可能数
* そのレシピを何人分調理可能か
*/
private int maxServings;
/** /**
* 食材リスト * 食材リスト

@ -27,6 +27,7 @@ import com.example.todoapp.model.Stuffs;
import com.example.todoapp.repository.RecipeStuffsRepository; import com.example.todoapp.repository.RecipeStuffsRepository;
import com.example.todoapp.repository.RecipesRepository; import com.example.todoapp.repository.RecipesRepository;
import com.example.todoapp.repository.StuffsRepository; import com.example.todoapp.repository.StuffsRepository;
import com.example.todoapp.repository.UserRepository;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@ -140,7 +141,7 @@ public class RecipeService {
* @return レシピ詳細情報レシピ基本情報と関連食材情報 * @return レシピ詳細情報レシピ基本情報と関連食材情報
* @throws RuntimeException レシピが見つからない場合 * @throws RuntimeException レシピが見つからない場合
*/ */
public RecipeDetailDTO getRecipeDetailsById(Long recipeId) { public RecipeDetailDTO getRecipeDetailsById(Long userId, Long recipeId) {
Recipes recipe = recipesRepository.findById(recipeId) Recipes recipe = recipesRepository.findById(recipeId)
.orElseThrow(() -> new RuntimeException("レシピが見つかりません")); .orElseThrow(() -> new RuntimeException("レシピが見つかりません"));
@ -162,6 +163,7 @@ public class RecipeService {
dto.setRecipeId(recipe.getRecipeId()); dto.setRecipeId(recipe.getRecipeId());
dto.setRecipeName(recipe.getRecipeName()); dto.setRecipeName(recipe.getRecipeName());
dto.setSummary(recipe.getSummary()); dto.setSummary(recipe.getSummary());
dto.setMaxServings(getRecipeMaxServings(userId, recipe));
dto.setStuffAndAmountArray(stuffList); dto.setStuffAndAmountArray(stuffList);
return dto; return dto;

@ -0,0 +1,124 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
} from '@mui/material';
import { toBuyApi } from '../services/api';
import { recipeApi } from '../services/api';
import CloseIcon from '@mui/icons-material/Close';
import { RecipeDetailWithId, StuffNameAndAmount } from '../types/types';
const AddByRecipeDialog = ({
openDialog,
setOpenDialog,
recipeId,
numOfPeople,
setNumOfPeaple,
checked,
setChecked
}: {
openDialog: boolean,
setOpenDialog: (open: boolean) => void,
recipeId: number,
numOfPeople: number,
setNumOfPeaple: (num: number) => void,
checked: boolean,
setChecked: (checked: boolean) => void
}) => {
const [recipe, setRecipe] = useState<RecipeDetailWithId>();
useEffect(() => {
console.log("called AddByRecipeDialog useEffect recipeId: ", recipeId);
if (recipeId) {
const fetchRecipe = async () => {
console.log("Fetching recipe with ID:", recipeId);
setRecipe(await recipeApi.getById(recipeId));
};
fetchRecipe();
}
}, [recipeId]);
return (
recipe ?
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minWidth: '300px', maxHeight: '80vh' } }}>
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<CloseIcon
onClick={() => setOpenDialog(false)}
sx={{
cursor: 'pointer',
color: (theme) => theme.palette.grey[500],
}}
/>
</DialogTitle>
<DialogContent dividers sx={{ padding: 2 }}>
<div>
<strong>:</strong> {recipe.recipeName}
</div>
<div>
({recipe.maxServings})
</div>
<div>
<strong>1:</strong>
<ul>
{recipe.stuffAndAmountArray.map((item, index) => (
<li key={index}>
{item.stuffName} - {item.amount}
</li>
))}
</ul>
</div>
{/* 在庫との差分を取るかのチェックボックス */}
<div>
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
</label>
</div>
{/* レシピを追加する人数分を入力 */}
<div>
<strong>:</strong>
<input
type="number"
min="1"
defaultValue={1}
onChange={(e) => {
setNumOfPeaple(parseInt(e.target.value, 10));
}}
/>
</div>
{/* 買うものリストに追加するボタン */}
<Box sx={{ mt: 2 }}>
<Button
variant="contained"
color="primary"
onClick={() => {
toBuyApi.addByRecipe(recipe.recipeId, numOfPeople, checked);
setOpenDialog(false);
}}
>
</Button>
</Box>
</DialogContent>
</Dialog>
: <></>
);
}
export default AddByRecipeDialog;

@ -27,10 +27,11 @@ import {
InputLabel InputLabel
} from '@mui/material'; } from '@mui/material';
import { import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon, Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, ShoppingBasket as ShoppingBasketIcon,
SoupKitchen as SoupKitchenIcon, Close as CloseIcon, TaskAlt as TaskAltIcon SoupKitchen as SoupKitchenIcon, Close as CloseIcon, TaskAlt as TaskAltIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { ToBuy, Stuff, RecipeWithId, RecipeDetail } from '../types/types'; import { ToBuy, Stuff, RecipeWithId, RecipeDetail } from '../types/types';
import AddByRecipeDialog from '../components/AddByRecipeDialog';
import { useMessage } from '../components/MessageContext'; import { useMessage } from '../components/MessageContext';
const RecipeList: React.FC = () => { const RecipeList: React.FC = () => {
@ -39,8 +40,20 @@ const RecipeList: React.FC = () => {
const { showErrorMessage } = useMessage(); const { showErrorMessage } = useMessage();
const EMPTY_RECIPEWITHID = {
recipeName: '',// レシピ名
summary: '',// レシピ概要
recipeId: 0, // レシピID
maxServings: 0 // 最大調理可能数
}
// すべての料理リスト // すべての料理リスト
const [allRecipes, setAllRecipes] = useState<RecipeWithId[]>(); const [allRecipes, setAllRecipes] = useState<RecipeWithId[]>([EMPTY_RECIPEWITHID, EMPTY_RECIPEWITHID]);
const [openAddByRecipeDialog, setOpenAddByRecipeDialog] = useState(false);
const [addByRecipeId, setAddByRecipeId] = useState(0);
const [numOfPeople, setNumOfPeople] = useState(1);
const [checked, setChecked] = useState(false);
const openRecipeById = (recipeId: number) => { const openRecipeById = (recipeId: number) => {
navigate('/addRecipe/' + recipeId); navigate('/addRecipe/' + recipeId);
@ -51,7 +64,7 @@ const RecipeList: React.FC = () => {
const recipes = await recipeApi.getAllRecipes(); const recipes = await recipeApi.getAllRecipes();
setAllRecipes(recipes); setAllRecipes(recipes);
console.log("マックス"); console.log("マックス");
console.log(recipes.map(recipe => recipe.maxServings)) console.log(allRecipes.map(recipe => recipe.maxServings))
} catch (error) { } catch (error) {
showErrorMessage("レシピの取得に失敗しました。"); showErrorMessage("レシピの取得に失敗しました。");
// console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error); // console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error);
@ -64,6 +77,7 @@ const RecipeList: React.FC = () => {
}, []); }, []);
return ( return (
<>
<Container> <Container>
<Box> <Box>
<div> <div>
@ -89,10 +103,47 @@ const RecipeList: React.FC = () => {
boxShadow: 1, boxShadow: 1,
fontSize: '40px' fontSize: '40px'
}} }}
onClick={() => openRecipeById(recipe.recipeId)} onClick = {() =>
> {
console.log("recipeId: ", recipe.recipeId);
setAddByRecipeId(recipe.recipeId)
setOpenAddByRecipeDialog(true)}
}
>
{recipe.recipeName} {recipe.recipeName}
{recipe.maxServings === 0 && <CloseIcon />} {recipe.maxServings === 0 && <FormGroup row><CloseIcon
style={{fontSize:"3vw", position: "absolute", right:'10%', transform: 'translateY(-50%)', color: "tomato"}}
/>
<Tooltip title="編集">
<IconButton sx={{ position: "absolute", right:'3%', transform: 'translateY(-50%)' }} edge="end" aria-label="編集"
onClick={() => {
navigate('/addRecipe/' + recipe.recipeId);
}}
>
<EditIcon style={{fontSize:"2.5vw"}}/>
</IconButton>
</Tooltip>
</FormGroup>}
{recipe.maxServings !== 0 && <FormGroup row>
<text style={{
fontSize:"2vw", position: "absolute", right:'18%', transform: 'translateY(-52%)', color: "mediumaquamarine"}}>
{recipe.maxServings}
</text>
<TaskAltIcon style={{
fontSize:"3vw", position: "absolute", right:'10%', transform: 'translateY(-50%)', color: "mediumaquamarine"}}
/>
{/* 買い物リスト:数量変更ボタン */}
<Tooltip title="編集">
<IconButton sx={{ position: "absolute", right:'3%', transform: 'translateY(-50%)' }} edge="end" aria-label="編集"
onClick={() => {
navigate('/addRecipe/' + recipe.recipeId);
}}
>
<EditIcon style={{fontSize:"2.5vw"}}/>
</IconButton>
</Tooltip>
</FormGroup>
}
</Button> </Button>
</FormGroup> </FormGroup>
// </ListItem> // </ListItem>
@ -100,7 +151,19 @@ const RecipeList: React.FC = () => {
{/* </List> */} {/* </List> */}
</div> </div>
<div style={{ width: "100%", position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "64px" }}> <div style={{ width: "100%", position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "64px" }}>
<Button variant='contained' sx={{ <Box sx={{ textAlign: 'center', position: 'fixed', bottom: 66, left: '80%', transform: 'translateX(-50%)' }}>
<Typography variant="caption" color="textSecondary">
</Typography>
</Box>
<Fab
color="primary"
sx={{ position: 'fixed', bottom: 90, left: '80%', transform: 'translateX(-50%)' }}
onClick={() => navigate('/addRecipe')}
>
<AddIcon />
</Fab>
{/* <Button variant='contained' sx={{
width: "60%", height: "60px", width: "60%", height: "60px",
fontSize: "32px", left: "50%", transform: 'translateX(-50%)' fontSize: "32px", left: "50%", transform: 'translateX(-50%)'
}} }}
@ -108,11 +171,20 @@ const RecipeList: React.FC = () => {
onClick={() => navigate('/addRecipe')} onClick={() => navigate('/addRecipe')}
> >
</Button> </Button> */}
</div> </div>
</Box> </Box>
</Container> </Container>
{/* 買うものリストへ追加ダイアログ */}
{
<AddByRecipeDialog openDialog={openAddByRecipeDialog} setOpenDialog={setOpenAddByRecipeDialog}
recipeId = {addByRecipeId} numOfPeople={numOfPeople} setNumOfPeaple={setNumOfPeople}
checked = {checked} setChecked={setChecked}
/>
}
</>
); );
}; };

@ -3,7 +3,7 @@
* APIとの通信を担当するモジュール * APIとの通信を担当するモジュール
* *
*/ */
import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, RecipeDetail, StuffAndCategoryAndAmount, RecipeWithId, StockHistory, StockUpdateRequest } from '../types/types'; import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, RecipeDetail, StuffAndCategoryAndAmount, RecipeWithId, StockHistory, StockUpdateRequest, RecipeDetailWithId } from '../types/types';
import { AUTH_ERRORS, TOBUY_ERRORS, STOCK_ERRORS, RECIPE_ERRORS } from '../constants/errorMessages'; import { AUTH_ERRORS, TOBUY_ERRORS, STOCK_ERRORS, RECIPE_ERRORS } from '../constants/errorMessages';
// APIのベースURL - 環境変数から取得するか、デフォルト値を使用 // APIのベースURL - 環境変数から取得するか、デフォルト値を使用
@ -469,12 +469,7 @@ export const recipeApi = {
* @param recipeId ID * @param recipeId ID
* @returns * @returns
*/ */
getById: async (recipeId: number): Promise<{ getById: async (recipeId: number): Promise<RecipeDetailWithId> => {
recipeId: number;
recipeName: string;
summary: string;
stuffAndAmountArray: StuffAndCategoryAndAmount[];
}> => {
const response = await fetch(`${API_BASE_URL}/api/recipes/getById?recipeId=${recipeId}`, { const response = await fetch(`${API_BASE_URL}/api/recipes/getById?recipeId=${recipeId}`, {
method: 'GET', method: 'GET',
headers: getHeaders(), headers: getHeaders(),

@ -160,4 +160,21 @@ export interface RecipeDetail extends Recipe {
category?: string; // 新規材料カテゴリ(オプション) category?: string; // 新規材料カテゴリ(オプション)
amount: number; // 使用量(必須) amount: number; // 使用量(必須)
}[]; }[];
}
/**
*
* (ID, 調)
*/
export interface RecipeDetailWithId extends RecipeWithId {
// 材料リスト(直接配列として内包)
stuffAndAmountArray: {
// 既存材料IDまたは新規作成情報のどちらか一方のみ必要
stuffId: number; // 材料ID
stuffName: string; // 材料名
category: string; // 材料カテゴリ
amount: number; // 数量
}[];
} }
Loading…
Cancel
Save