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 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 ac5aa14..2bb15fc 100644 --- a/frontend/src/pages/AddRecipe.tsx +++ b/frontend/src/pages/AddRecipe.tsx @@ -26,7 +26,8 @@ import { Select, FormControl, InputLabel, - ListItemIcon + ListItemIcon, + AlertColor } from '@mui/material'; import { Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon, @@ -37,6 +38,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(); @@ -44,6 +47,8 @@ const AddRecipe: React.FC = () => { const navigate = useNavigate(); + // 編集時,既存情報を読み込んだかどうか + const [recipeLoaded, setRecipeLoaded] = useState(false); // 何人分かを格納する const [numOfPeaple, setNumOfPeaple] = useState(1); const [openNumOfPeapleDialog, setOpenNumOfPeapleDialog] = useState(false); @@ -62,48 +67,59 @@ 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('レシピ名が入力されていません!') // console.log("yes1"); // return false; // } // if (!items.length) { - // alert('材料が追加されていません!') + // showErrorMessage('材料が追加されていません!') // console.log("yes2"); // return false; // } - if (!recipeId) { - console.log("yes3"); + try { + if (!recipeId) { + console.log("yes3"); // 新規追加 - const response = await recipeApi.addRecipe({ + 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, - }) console.log("yes4"); return recipeId; } @@ -126,8 +142,8 @@ const AddRecipe: React.FC = () => { const handleSubmit = async () => { const recipeId = await handleSaveRecipe(); - // alert('レシピが保存されました!'); if (!recipeId) return; + showSuccessMessage('レシピが保存されました!'); navigate('/recipeList'); } @@ -139,7 +155,7 @@ const AddRecipe: React.FC = () => { if (!recipeId) return false; console.log("ds"); await toBuyApi.addByRecipe(recipeId); - // alert('レシピが保存されて買うものリストに追加されました!'); + showSuccessMessage('レシピが保存されて買うものリストに追加されました!'); navigate('/tasks'); } @@ -161,7 +177,8 @@ const AddRecipe: React.FC = () => { }, []); return ( - (recipeId && !recipeName) + <> + {(recipeId && !recipeLoaded) ?

読み込み中...

: @@ -218,7 +235,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) + }}> @@ -240,7 +261,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 = () => { ))} {/* */} -
-
-
+
- - + /> */} + + + + )} @@ -375,13 +427,26 @@ const StockPage: React.FC = () => { <> 【{selectedRow.stuffName}】を削除します。 ⚠️ 注意: 削除すると復元できません。 - - + + + + + )} @@ -398,6 +463,7 @@ const StockPage: React.FC = () => { + {/* */} {/* 在庫の食材追加ボタン */} {/* 在庫の食材削除ボタン (全テーブル共通) */} diff --git a/frontend/src/pages/TaskListPage.tsx b/frontend/src/pages/TaskListPage.tsx index 74b756d..fd2335f 100644 --- a/frontend/src/pages/TaskListPage.tsx +++ b/frontend/src/pages/TaskListPage.tsx @@ -31,6 +31,7 @@ import AddStuffAmountDialog from '../components/AddStuffAmountDialog'; import BuyDialog from '../components/BuyDialog'; import { useNavigate } from 'react-router-dom'; import DatePicker from 'react-datepicker'; +import { useMessage } from '../components/MessageContext'; //import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留 @@ -91,7 +92,7 @@ const TaskListPage: React.FC = () => { shop: undefined, }); - + const { showErrorMessage } = useMessage(); // コンポーネントマウント時にタスク一覧を取得 useEffect(() => { @@ -149,7 +150,7 @@ const TaskListPage: React.FC = () => { const handleAddNewToBuy = async () => { try { if (isNaN(newToBuy.amount)) { - console.log('数量が正しくありません.'); + showErrorMessage('数量が正しくありません.'); return; } @@ -171,7 +172,7 @@ const TaskListPage: React.FC = () => { const handleUpdateNewToBuy = async () => { try { if (isNaN(editingItem.amount)) { - console.log('数量が正しくありません.'); + showErrorMessage('数量が正しくありません.'); return; } @@ -195,7 +196,7 @@ const TaskListPage: React.FC = () => { console.log("newPrice:", newStock.price) console.log("parsedPrice: ", parsedPrice) if (isNaN(parsedPrice)) { - alert('入力が無効です') + showErrorMessage('価格が正しく入力されていません。') return //setNewStock({ ...newStock, price: parsedPrice }); } @@ -251,6 +252,7 @@ const TaskListPage: React.FC = () => { { setOpenInfoDialog(true) + setEditingItem(tobuy) setSelectedToBuyId(tobuy.tobuyId) // handleDeleteTask(tobuy.tobuyId) }}> @@ -294,28 +296,28 @@ const TaskListPage: React.FC = () => {
{/* 新規材料作成ボタン - 画面下部に固定表示 */} - + 材料のみ追加 setOpenAddToBuyDialog(true)} > {/*新規料理追加ボタン - 画面下部に固定表示 */} - + 料理から追加 { // setOpenAddToBuyDialog(true); navigate('/RecipeList'); @@ -330,7 +332,7 @@ const TaskListPage: React.FC = () => { {/* 購入処理(在庫登録)のための数値入力ダイアログ */} - + {/* 数量変更ダイアログ */} => { + addByRecipe: async (recipeId: number, servings: number): Promise => { const response = await fetch(`${API_BASE_URL}/api/tobuy/addByRecipe`, { method: 'POST', headers: getHeaders(), - body: JSON.stringify({ recipeId }), + body: JSON.stringify({ recipeId, servings }), }) if (!response.ok) { throw new Error(TOBUY_ERRORS.CREATE_FAILED);