diff --git a/backend/src/main/java/com/example/todoapp/controller/ToBuysController.java b/backend/src/main/java/com/example/todoapp/controller/ToBuysController.java index 799d3f9..d5753e9 100644 --- a/backend/src/main/java/com/example/todoapp/controller/ToBuysController.java +++ b/backend/src/main/java/com/example/todoapp/controller/ToBuysController.java @@ -92,7 +92,6 @@ public class ToBuysController { } - /** * 指定されたユーザーIDに基づいてすべての「買うもの」リストを取得する * @@ -125,9 +124,6 @@ public class ToBuysController { }) .collect(Collectors.toList()); - // Map responseBody = new HashMap<>(); - // responseBody.put("tobuy_array", responseList); - return ResponseEntity.ok(responseList); } @@ -189,7 +185,8 @@ public class ToBuysController { Authentication authentication) { Long recipeId = payload.get("recipeId"); - List responseList = toBuysService.addByRecipeId(recipeId, authentication); + Long servings = payload.get("servings"); + List responseList = toBuysService.addByRecipeId(recipeId, servings,authentication); //shopのフィールドを削除 List> filteredList = responseList.stream() diff --git a/backend/src/main/java/com/example/todoapp/service/StocksService.java b/backend/src/main/java/com/example/todoapp/service/StocksService.java index bd2dc19..51a142f 100644 --- a/backend/src/main/java/com/example/todoapp/service/StocksService.java +++ b/backend/src/main/java/com/example/todoapp/service/StocksService.java @@ -1,6 +1,5 @@ package com.example.todoapp.service; -import com.example.todoapp.dto.StockDTO; import com.example.todoapp.dto.AddStocksDTO; import com.example.todoapp.dto.UpdateStockRequest; import com.example.todoapp.model.Stocks; @@ -57,9 +56,6 @@ public class StocksService { } else { // 材料情報を取得 Optional existstuffs = stuffsRepository.findById(stock.getStuffId()); - if (existstuffs == null) { - throw new RuntimeException("材料がありません"); - } stuffs = existstuffs.get(); } diff --git a/backend/src/main/java/com/example/todoapp/service/ToBuysService.java b/backend/src/main/java/com/example/todoapp/service/ToBuysService.java index 678aae6..fe3bfe7 100644 --- a/backend/src/main/java/com/example/todoapp/service/ToBuysService.java +++ b/backend/src/main/java/com/example/todoapp/service/ToBuysService.java @@ -91,14 +91,22 @@ public class ToBuysService { stuff = optStuff.get(); } - ToBuys toBuys = new ToBuys(); - toBuys.setUser(user); - toBuys.setStuff(stuff); - toBuys.setAmount(toBuyDTO.getAmount()); - toBuys.setStore(toBuyDTO.getShop()); + Optional existingToBuy = toBuysRepository.findByUserAndStuff(user, stuff); - // データベースに保存 - return toBuysRepository.save(toBuys); + if (existingToBuy.isPresent()) { + // 存在する場合は数量を更新 + ToBuys existing = existingToBuy.get(); + existing.setAmount(existing.getAmount() + toBuyDTO.getAmount()); + return toBuysRepository.save(existing); + } else { + // 新しい材料を作成 + ToBuys toBuys = new ToBuys(); + toBuys.setUser(user); + toBuys.setStuff(stuff); + toBuys.setAmount(toBuyDTO.getAmount()); + toBuys.setStore(toBuyDTO.getShop()); + return toBuysRepository.save(toBuys); + } } @@ -212,7 +220,7 @@ public class ToBuysService { * @param authentication 認証情報 * @return 追加された「買うもの」のリスト */ - public List addByRecipeId(Long recipeId, Authentication authentication) { + public List addByRecipeId(Long recipeId, Long servings,Authentication authentication) { // ユーザー情報を取得 String username = authentication.getName(); User user = userRepository.findByUsername(username) @@ -225,7 +233,9 @@ public class ToBuysService { for (RecipeStuffs rs : recipeStuffsList) { Stuffs stuff = rs.getStuff(); - int requiredAmount = rs.getAmount(); + + // 材料の数量をサービング数に基づいて計算 + int requiredAmount = rs.getAmount() * (servings != null ? servings.intValue() : 1); Optional existingToBuyOpt = toBuysRepository.findByUserAndStuff(user, stuff); diff --git a/backend/src/main/resources/application-localfwd.yml b/backend/src/main/resources/application-localfwd.yml new file mode 100644 index 0000000..536f15b --- /dev/null +++ b/backend/src/main/resources/application-localfwd.yml @@ -0,0 +1,20 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:5432/${LOCAL_DB_NAME} + driver-class-name: org.postgresql.Driver + username: ${LOCAL_DB_USER} + password: ${LOCAL_DB_PASSWORD} + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + properties: + hibernate: + jdbc: + lob: + non_contextual_creation: true + +server: + address: 0.0.0.0 + port: 8080 + +cors: + allowed-origins: http://${WIN_IP}:3000 \ No newline at end of file diff --git a/frontend/src/components/BuyDialog.tsx b/frontend/src/components/BuyDialog.tsx index 6acfdd3..27485fe 100644 --- a/frontend/src/components/BuyDialog.tsx +++ b/frontend/src/components/BuyDialog.tsx @@ -26,12 +26,14 @@ const formatDateLocal = (date: Date) => { const BuyDialog = ({ openDialog, setOpenDialog, + stuffName, newStock, setNewStock, onSubmit, }: { openDialog: boolean, setOpenDialog: (open: boolean) => void, + stuffName: string, newStock: NewStock, setNewStock: (tobuy: NewStock) => void, onSubmit: () => void, @@ -40,11 +42,21 @@ const BuyDialog = ({ return ( - setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '500px', maxHeight: '80vh' } }} + setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '600px', maxHeight: '80vh' } }} > 在庫登録 + {/* 材料名表示 */} + + {/* 価格入力フィールド */} {/* 購入日・消費期限を横並びに */} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 4053729..fe8bff1 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -17,6 +17,7 @@ import { ListItemButton, Divider, IconButton, + AlertColor, BottomNavigation, BottomNavigationAction, Paper @@ -30,6 +31,8 @@ import { ShoppingCart as ShoppingCartIcon } from '@mui/icons-material'; import { useNavigate, Outlet, useLocation } from 'react-router-dom'; +import { MessageContext } from './MessageContext'; +import MessageAlert from './MessageAlert'; const Layout: React.FC = () => { const navigate = useNavigate(); @@ -78,6 +81,39 @@ const Layout: React.FC = () => { setDrawerOpen(!drawerOpen); }; + // メッセージ表示 + + // ページ遷移後もメッセージを維持 + useEffect(() => { + const saved = sessionStorage.getItem('globalMessage'); + if (saved) { + const { message, severity } = JSON.parse(saved); + showMessage(message, severity); + } + }, []); + + const [msgOpen, setMsgOpen] = useState(false); + const [msgText, setMsgText] = useState(''); + const [msgType, setMsgType] = useState('info'); + + const showMessage = (msg: string, sev: AlertColor) => { + setMsgText(msg); + setMsgType(sev); + setMsgOpen(true); + sessionStorage.setItem('globalMessage', JSON.stringify({ message: msg, severity: sev })); + }; + + const showErrorMessage = (message: string) => showMessage(message, 'error'); + const showWarningMessage = (message: string) => showMessage(message, 'warning'); + const showInfoMessage = (message: string) => showMessage(message, 'info'); + const showSuccessMessage = (message: string) => showMessage(message, 'success'); + + const handleMsgClose = () => { + setMsgOpen(false); + // setMsgText(''); // ここで空にすると,メッセージが消えるアニメーションが始まる時点で文字が消えてしまう + sessionStorage.removeItem('globalMessage'); + }; + return ( @@ -128,7 +164,15 @@ const Layout: React.FC = () => { {/* メインコンテンツ領域 - 子ルートのコンポーネントがここに表示される */} + + {/* React Router の Outlet - 子ルートのコンポーネントがここにレンダリングされる */} + @@ -136,4 +180,4 @@ const Layout: React.FC = () => { }; - export default Layout; \ No newline at end of file + export default Layout; diff --git a/frontend/src/components/MessageAlert.tsx b/frontend/src/components/MessageAlert.tsx new file mode 100644 index 0000000..7368571 --- /dev/null +++ b/frontend/src/components/MessageAlert.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Snackbar, Alert, AlertColor } from '@mui/material'; + +interface MessageAlertProps { + open: boolean; + message: string; + severity: AlertColor; // 'error' | 'warning' | 'info' | 'success' + onClose: (event?: React.SyntheticEvent | Event, reason?: string) => void; + duration?: number; +} + +const MessageAlert: React.FC = ({ + open, + message, + severity, + onClose, + duration = 6000, +}) => { + return ( + + + {message} + + + ); +}; + +export default MessageAlert; diff --git a/frontend/src/components/MessageContext.tsx b/frontend/src/components/MessageContext.tsx new file mode 100644 index 0000000..599f2a5 --- /dev/null +++ b/frontend/src/components/MessageContext.tsx @@ -0,0 +1,17 @@ + +import React, { createContext, useContext } from 'react'; + +export interface MessageContextType { + showErrorMessage: (message: string) => void; + showWarningMessage: (message: string) => void; + showSuccessMessage: (message: string) => void; + showInfoMessage: (message: string) => void; +} + +export const MessageContext = createContext(undefined); + +export const useMessage = () => { + const context = useContext(MessageContext); + if (!context) throw new Error('useMessage must be used within MessageContext.Provider'); + return context; +}; diff --git a/frontend/src/pages/AddRecipe.tsx b/frontend/src/pages/AddRecipe.tsx index ee570bd..8a0e880 100644 --- a/frontend/src/pages/AddRecipe.tsx +++ b/frontend/src/pages/AddRecipe.tsx @@ -25,7 +25,8 @@ import { Select, FormControl, InputLabel, - ListItemIcon + ListItemIcon, + AlertColor } from '@mui/material'; import { Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon, @@ -36,6 +37,8 @@ import { StuffAndCategoryAndAmount } from '../types/types'; import EditAmountDialog from '../components/EditAmountDialog'; import { recipeApi, toBuyApi } from '../services/api'; import { useNavigate, useParams } from 'react-router-dom'; +import MessageAlert from '../components/MessageAlert'; +import { useMessage } from '../components/MessageContext'; const AddRecipe: React.FC = () => { const { recipeId: recipeIdStr } = useParams(); @@ -43,6 +46,8 @@ const AddRecipe: React.FC = () => { const navigate = useNavigate(); + // 編集時,既存情報を読み込んだかどうか + const [recipeLoaded, setRecipeLoaded] = useState(false); // 料理名,説明 const [recipeName, setRecipeName] = useState(''); const [recipeSummary, setRecipeSummary] = useState(''); @@ -61,52 +66,61 @@ const AddRecipe: React.FC = () => { //削除確認ダイアログの表示状態 const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + // エラーメッセージ表示 + const { showErrorMessage, showSuccessMessage } = useMessage(); + const loadRecipe = async () => { - if (recipeId && !recipeName) { + if (recipeId && !recipeLoaded) { const recipe = await recipeApi.getById(recipeId); console.log('loaded recipe=', recipe) setRecipeName(recipe.recipeName) setRecipeSummary(recipe.summary) setItems(recipe.stuffAndAmountArray) + setRecipeLoaded(true) } } const handleSaveRecipe = async () => { if (!recipeName) { - alert('レシピ名が入力されていません!') + showErrorMessage('レシピ名が入力されていません!') return false; } if (!items.length) { - alert('材料が追加されていません!') + showErrorMessage('材料が追加されていません!') return false; } - if (!recipeId) { - // 新規追加 - const response = await recipeApi.addRecipe({ + try { + 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 response.recipeId; + } catch { + showErrorMessage('レシピの送信に失敗しました。同じ料理名が存在する可能性があります。'); + return false; } - const response = await recipeApi.updateRecipe({ - recipeId, - recipeName, - summary: recipeSummary, - stuffAndAmountArray: items, - }) - return recipeId; } const handleSubmit = async () => { const recipeId = await handleSaveRecipe(); - // alert('レシピが保存されました!'); if (!recipeId) return; + showSuccessMessage('レシピが保存されました!'); navigate('/recipeList'); } @@ -114,7 +128,7 @@ const AddRecipe: React.FC = () => { const recipeId = await handleSaveRecipe(); if (!recipeId) return false; await toBuyApi.addByRecipe(recipeId); - // alert('レシピが保存されて買うものリストに追加されました!'); + showSuccessMessage('レシピが保存されて買うものリストに追加されました!'); navigate('/tasks'); } @@ -124,7 +138,8 @@ const AddRecipe: React.FC = () => { }, []); return ( - (recipeId && !recipeName) + <> + {(recipeId && !recipeLoaded) ?

読み込み中...

: @@ -206,7 +221,7 @@ const AddRecipe: React.FC = () => { -
+
-
-
-
+