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 ee022aa..cc45335 100644 --- a/backend/src/main/java/com/example/todoapp/service/StocksService.java +++ b/backend/src/main/java/com/example/todoapp/service/StocksService.java @@ -56,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 fa90b27..75a6d10 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -2,7 +2,7 @@ * アプリケーションの共通レイアウトを提供するコンポーネント * ヘッダー(AppBar)とメインコンテンツ領域を含む基本的なページ構造を定義 */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AppBar, Toolbar, @@ -16,7 +16,11 @@ import { ListItemIcon, ListItemButton, Divider, - IconButton + IconButton, + AlertColor, + BottomNavigation, + BottomNavigationAction, + Paper } from '@mui/material'; import { Menu as MenuIcon, @@ -24,13 +28,17 @@ import { Inventory as InventoryIcon, // テストページ用のアイコン Science as ScienceIcon, // 鈴木 SoupKitchen as SoupKitchenIcon, + 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(); const location = useLocation(); const [drawerOpen, setDrawerOpen] = useState(false); + const [bottomNavi, setBottomNavi] = useState(1); /** * ログアウト処理を行うハンドラー関数 @@ -41,39 +49,69 @@ const Layout: React.FC = () => { navigate('/login'); }; - /** - * 画面遷移処理を行うハンドラー関数 - * 指定されたパスに遷移し、サイドメニューを閉じる - */ - const handleNavigate = (path: string) => { - navigate(path); - setDrawerOpen(false); - }; + - // 現在のパスに基づいてメニュー項目が選択状態かどうかを判定 - const isSelected = (path: string): boolean => { - return location.pathname === path; - }; + // メニューを開閉するハンドラー const toggleDrawer = () => { 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'); + }; + + const handleBottomNavigation = (event: React.SyntheticEvent, newValue: any) => { + setBottomNavi(newValue); + switch(newValue) { + case 0: + navigate('stock'); + break; + case 1: + navigate('tasks'); + break; + case 2: + navigate('recipeList'); + break; + } + // ここでルーティング処理などを行う + } + return ( {/* ヘッダー部分 - アプリ名とログアウトボタンを表示 */} - - - shopchop @@ -83,57 +121,46 @@ const Layout: React.FC = () => { - {/* サイドメニュー */} - setDrawerOpen(false)} - > - + { + setBottomNavi(newValue); + switch(newValue) { + case 0: + navigate('stock'); + break; + case 1: + navigate('tasks'); + break; + case 2: + navigate('recipeList'); + break; + } + // ここでルーティング処理などを行う + }} > - - handleNavigate('/tasks')} - selected={isSelected('/tasks')} - > - - - - {/* テストページへのリンクを追加 */} - - {/* 在庫リストへのリンクを追加 */} - handleNavigate('/addRecipe')} - selected={isSelected('/addRecipe')} - > - - - - handleNavigate('/recipeList')} - selected={isSelected('/recipeList')} - > - - - - handleNavigate('/stock')} - selected={isSelected('/stock')} - > - - - - - - - + } /> + } /> + } /> + + {/* メインコンテンツ領域 - 子ルートのコンポーネントがここに表示される */} - {/* React Router の Outlet - 子ルートのコンポーネントがここにレンダリングされる */} + + + + {/* React Router の Outlet - 子ルートのコンポーネントがここにレンダリングされる */} + + 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 2618fda..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(''); @@ -58,53 +63,64 @@ const AddRecipe: React.FC = () => { // 編集しているアイテム const [editingItem, setEditingItem] = useState(emptyItem); const [editingItemIdx, setEditingItemIdx] = useState(0); + //削除確認ダイアログの表示状態 + 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'); } @@ -112,7 +128,7 @@ const AddRecipe: React.FC = () => { const recipeId = await handleSaveRecipe(); if (!recipeId) return false; await toBuyApi.addByRecipe(recipeId); - // alert('レシピが保存されて買うものリストに追加されました!'); + showSuccessMessage('レシピが保存されて買うものリストに追加されました!'); navigate('/tasks'); } @@ -122,7 +138,8 @@ const AddRecipe: React.FC = () => { }, []); return ( - (recipeId && !recipeName) + <> + {(recipeId && !recipeLoaded) ?

読み込み中...

: @@ -138,7 +155,7 @@ const AddRecipe: React.FC = () => { value={recipeSummary} onChange={(e) => setRecipeSummary(e.target.value)} /> -

+

材料リスト

{/* すべての材料情報を表示 */} @@ -179,7 +196,11 @@ const AddRecipe: React.FC = () => { tooltip: { sx: { backgroundColor: "white", color: "red", fontSize: "0.8rem", padding: "6px", borderRadius: "6px" } }, }}> setItems([...items.slice(0, index), ...items.slice(index + 1)])}> + onClick={() => { + setOpenDeleteDialog(true) + setEditingItem(item) + setEditingItemIdx(index) + }}> @@ -200,7 +221,7 @@ const AddRecipe: React.FC = () => { -
+
+ + + )} + +
+ {/* 数量変更ダイアログ */} { }} /> + } + ); }; diff --git a/frontend/src/pages/DishList.tsx b/frontend/src/pages/DishList.tsx index 2202f23..1a28b76 100644 --- a/frontend/src/pages/DishList.tsx +++ b/frontend/src/pages/DishList.tsx @@ -159,9 +159,10 @@ const DishList: React.FC = () => { ))} {/* */} -
-
-
+