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. 88
      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(
Authentication authentication,
@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 {
response.put("result", false);
response.put("message", "編集に失敗しました");
}
return ResponseEntity.ok(response);
}

@ -31,6 +31,12 @@ public class RecipeDetailDTO {
*/
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.RecipesRepository;
import com.example.todoapp.repository.StuffsRepository;
import com.example.todoapp.repository.UserRepository;
import jakarta.transaction.Transactional;
@ -140,7 +141,7 @@ public class RecipeService {
* @return レシピ詳細情報レシピ基本情報と関連食材情報
* @throws RuntimeException レシピが見つからない場合
*/
public RecipeDetailDTO getRecipeDetailsById(Long recipeId) {
public RecipeDetailDTO getRecipeDetailsById(Long userId, Long recipeId) {
Recipes recipe = recipesRepository.findById(recipeId)
.orElseThrow(() -> new RuntimeException("レシピが見つかりません"));
@ -162,6 +163,7 @@ public class RecipeService {
dto.setRecipeId(recipe.getRecipeId());
dto.setRecipeName(recipe.getRecipeName());
dto.setSummary(recipe.getSummary());
dto.setMaxServings(getRecipeMaxServings(userId, recipe));
dto.setStuffAndAmountArray(stuffList);
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
} from '@mui/material';
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
} from '@mui/icons-material';
import { ToBuy, Stuff, RecipeWithId, RecipeDetail } from '../types/types';
import AddByRecipeDialog from '../components/AddByRecipeDialog';
import { useMessage } from '../components/MessageContext';
const RecipeList: React.FC = () => {
@ -39,8 +40,20 @@ const RecipeList: React.FC = () => {
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) => {
navigate('/addRecipe/' + recipeId);
@ -51,7 +64,7 @@ const RecipeList: React.FC = () => {
const recipes = await recipeApi.getAllRecipes();
setAllRecipes(recipes);
console.log("マックス");
console.log(recipes.map(recipe => recipe.maxServings))
console.log(allRecipes.map(recipe => recipe.maxServings))
} catch (error) {
showErrorMessage("レシピの取得に失敗しました。");
// console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error);
@ -64,6 +77,7 @@ const RecipeList: React.FC = () => {
}, []);
return (
<>
<Container>
<Box>
<div>
@ -89,10 +103,47 @@ const RecipeList: React.FC = () => {
boxShadow: 1,
fontSize: '40px'
}}
onClick={() => openRecipeById(recipe.recipeId)}
onClick = {() =>
{
console.log("recipeId: ", recipe.recipeId);
setAddByRecipeId(recipe.recipeId)
setOpenAddByRecipeDialog(true)}
}
>
{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>
</FormGroup>
// </ListItem>
@ -100,7 +151,19 @@ const RecipeList: React.FC = () => {
{/* </List> */}
</div>
<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",
fontSize: "32px", left: "50%", transform: 'translateX(-50%)'
}}
@ -108,11 +171,20 @@ const RecipeList: React.FC = () => {
onClick={() => navigate('/addRecipe')}
>
</Button>
</Button> */}
</div>
</Box>
</Container>
{/* 買うものリストへ追加ダイアログ */}
{
<AddByRecipeDialog openDialog={openAddByRecipeDialog} setOpenDialog={setOpenAddByRecipeDialog}
recipeId = {addByRecipeId} numOfPeople={numOfPeople} setNumOfPeaple={setNumOfPeople}
checked = {checked} setChecked={setChecked}
/>
}
</>
);
};

@ -3,7 +3,7 @@
* 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';
// APIのベースURL - 環境変数から取得するか、デフォルト値を使用
@ -469,12 +469,7 @@ export const recipeApi = {
* @param recipeId ID
* @returns
*/
getById: async (recipeId: number): Promise<{
recipeId: number;
recipeName: string;
summary: string;
stuffAndAmountArray: StuffAndCategoryAndAmount[];
}> => {
getById: async (recipeId: number): Promise<RecipeDetailWithId> => {
const response = await fetch(`${API_BASE_URL}/api/recipes/getById?recipeId=${recipeId}`, {
method: 'GET',
headers: getHeaders(),

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