From 105ddef9acc8d7d02f9019535de20c1fa716420c Mon Sep 17 00:00:00 2001 From: MurakawaTakuya Date: Sun, 19 Jan 2025 20:53:21 +0900 Subject: [PATCH] =?UTF-8?q?logger=E3=81=AB=E3=83=AA=E3=82=AF=E3=82=A8?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=AE=E8=A9=B3=E7=B4=B0=E3=82=92=E8=A8=98?= =?UTF-8?q?=E9=8C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/src/index.ts | 102 ++++++++++++------- functions/src/routers/goalRouter.ts | 89 ++++++++++++++-- functions/src/routers/postRouter.ts | 73 +++++++++++-- functions/src/routers/reactionRouter.ts | 18 +++- functions/src/routers/resultRouter.ts | 11 +- functions/src/routers/userRouter.ts | 80 +++++++++++++-- functions/src/tasks.ts | 16 +-- src/Components/GoalModal/CreateGoalModal.tsx | 2 +- src/Components/GoalModal/EditGoalModal.tsx | 2 +- 9 files changed, 318 insertions(+), 75 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index 8d1220e..5b786b0 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,5 +1,5 @@ import cors from "cors"; -import express from "express"; +import express, { Request } from "express"; import { rateLimit } from "express-rate-limit"; import admin from "firebase-admin"; import { logger } from "firebase-functions"; @@ -21,7 +21,7 @@ import resultRouter from "./routers/resultRouter"; import userRouer from "./routers/userRouter"; const app = express(); -app.set("trust proxy", true); // trueにしないと全ユーザーでlimitが共通になる +app.set("trust proxy", false); app.use(helmet()); app.use(cors()); app.use(express.json()); @@ -41,13 +41,15 @@ const verifyAppCheckToken = async ( } try { - const decodedToken = await admin - .appCheck() - .verifyToken(appCheckToken as string); - console.log("Verified App Check Token:", decodedToken); + await admin.appCheck().verifyToken(appCheckToken as string); return next(); } catch (error) { - logger.error(`Invalid App Check token with ${appCheckToken}:`, error); + logger.error({ + message: `Invalid App Check token with ${appCheckToken}:`, + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(401).send("Invalid App Check token."); } }; @@ -69,10 +71,10 @@ app.use( rateLimit({ windowMs: 10 * 60 * 1000, max: 200, - // keyGenerator: (req) => { - // const key = req.headers["x-forwarded-for"] || req.ip || "unknown"; - // return Array.isArray(key) ? key[0] : key; - // }, + keyGenerator: (req) => { + const key = req.headers["x-forwarded-for"] || req.ip || "unknown"; + return Array.isArray(key) ? key[0] : key; + }, handler: (req, res) => { return res .status(429) @@ -85,10 +87,10 @@ app.use( rateLimit({ windowMs: 60 * 60 * 1000, max: 500, - // keyGenerator: (req) => { - // const key = req.headers["x-forwarded-for"] || req.ip || "unknown"; - // return Array.isArray(key) ? key[0] : key; - // }, + keyGenerator: (req) => { + const key = req.headers["x-forwarded-for"] || req.ip || "unknown"; + return Array.isArray(key) ? key[0] : key; + }, handler: (req, res) => { return res .status(429) @@ -117,27 +119,28 @@ export { } from "./tasks"; // テスト用API -app.use( - "/helloWorld", - rateLimit({ - // 10分に最大10回に制限 - windowMs: 10 * 60 * 1000, - max: 10, - keyGenerator: (req) => { - const key = req.headers["x-forwarded-for"] || req.ip || "unknown"; - return Array.isArray(key) ? key[0] : key; - }, - handler: (req, res) => { - return res - .status(429) - .json({ message: "Too many requests, please try again later." }); - }, - }) -); +const helloWorldRateLimiter = rateLimit({ + windowMs: 10 * 60 * 1000, // 10分 + max: 10, // 最大10回 + keyGenerator: (req) => { + const key = req.headers["x-forwarded-for"] || req.ip || "unknown"; + return Array.isArray(key) ? key[0] : key; + }, + handler: (req, res) => { + return res + .status(429) + .json({ message: "Too many requests, please try again later." }); + }, +}); -export const helloWorld = onRequest({ region: region }, (req, res) => { - logger.info("Hello log!", { structuredData: true }); - res.send("Hello World!"); +export const helloWorld = onRequest({ region: region }, async (req, res) => { + helloWorldRateLimiter(req, res, async () => { + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); + res.send("Hello World!"); + }); }); const db = admin.firestore(); @@ -169,3 +172,32 @@ export const beforecreated = beforeUserCreated( return; } ); + +// リクエストのheader, parameters, bodyを取得 +export const getRequestData = (req: Request) => { + // requestLog + return { + headers: req.headers, + parameters: { + query: req.query, + params: req.params, + }, + body: req.body, + }; +}; + +// リクエストのhttp情報を取得 +export const getHttpRequestData = (req: Request) => { + // httpRequest + const statusCode = req.statusCode || "202"; + return { + requestMethod: req.method, + requestUrl: `${req.method} ${statusCode}: ${req.protocol}://${req.get( + "host" + )}${req.originalUrl}`, + status: statusCode, + userAgent: req.get("User-Agent"), + remoteIp: req.ip, + protocol: req.httpVersion ? `HTTP/${req.httpVersion}` : req.protocol, + }; +}; diff --git a/functions/src/routers/goalRouter.ts b/functions/src/routers/goalRouter.ts index bf499d9..586620d 100644 --- a/functions/src/routers/goalRouter.ts +++ b/functions/src/routers/goalRouter.ts @@ -1,6 +1,7 @@ import express, { Request, Response } from "express"; import admin from "firebase-admin"; import { logger } from "firebase-functions"; +import { getHttpRequestData, getRequestData } from ".."; import { Goal, GoalWithId } from "../types"; const router = express.Router(); @@ -9,6 +10,10 @@ const db = admin.firestore(); // GET: 全ての目標を取得 router.get("/", async (req: Request, res: Response) => { try { + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); const goalSnapshot = await db.collection("goal").get(); if (goalSnapshot.empty) { @@ -29,23 +34,33 @@ router.get("/", async (req: Request, res: Response) => { return res.json(goalData); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error fetching goals" }); } }); // GET: userIdから目標を取得 router.get("/:userId", async (req: Request, res: Response) => { - const userId = req.params.userId; - try { + const userId = req.params.userId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); + const goalSnapshot = await db .collection("goal") .where("userId", "==", userId) .get(); if (goalSnapshot.empty) { - return res.status(404).json({ message: "No goals found for this user" }); + return res + .status(404) + .json({ message: "No goals found for this user", userId }); } const goalData: GoalWithId[] = goalSnapshot.docs.map((doc) => { @@ -62,7 +77,11 @@ router.get("/:userId", async (req: Request, res: Response) => { return res.json(goalData); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error fetching goals" }); } }); @@ -75,8 +94,16 @@ router.post("/", async (req: Request, res: Response) => { try { ({ userId, deadline, text } = req.body as Goal); + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(400).json({ message: "Invalid request body" }); } @@ -99,11 +126,21 @@ router.post("/", async (req: Request, res: Response) => { post: null, }); + logger.info({ + message: "Goal created successfully", + goalId, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res .status(201) .json({ message: "Goal created successfully", goalId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error creating goal" }); } }); @@ -111,6 +148,10 @@ router.post("/", async (req: Request, res: Response) => { // PUT: 目標を更新 router.put("/:goalId", async (req: Request, res: Response) => { const goalId = req.params.goalId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); const { userId, deadline, text }: Partial = req.body; if (!goalId) { @@ -140,9 +181,19 @@ router.put("/:goalId", async (req: Request, res: Response) => { try { await db.collection("goal").doc(goalId).update(updateData); + logger.info({ + message: "Goal updated successfully", + goalId, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.json({ message: "Goal updated successfully", goalId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error updating goal" }); } }); @@ -151,6 +202,10 @@ router.put("/:goalId", async (req: Request, res: Response) => { router.delete("/:goalId", async (req: Request, res: Response) => { try { const goalId = req.params.goalId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); if (!goalId) { return res.status(400).json({ message: "goalId is required" }); @@ -172,15 +227,29 @@ router.delete("/:goalId", async (req: Request, res: Response) => { await file.delete(); logger.info("Image deleted successfully:", storedId); } catch (error) { - logger.error("Error deleting image:", error); + logger.error({ + error: `Error deleting image: ${error}`, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error deleting image" }); } } await goalRef.delete(); + logger.info({ + message: "Goal deleted successfully", + goalId, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.json({ message: "Goal deleted successfully", goalId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error deleting goal" }); } }); diff --git a/functions/src/routers/postRouter.ts b/functions/src/routers/postRouter.ts index 15da3f5..d920bbc 100644 --- a/functions/src/routers/postRouter.ts +++ b/functions/src/routers/postRouter.ts @@ -1,6 +1,7 @@ import express, { Request, Response } from "express"; import admin from "firebase-admin"; import { logger } from "firebase-functions"; +import { getHttpRequestData, getRequestData } from ".."; import { updateStreak } from "../status"; import { PostWithGoalId } from "../types"; @@ -10,6 +11,10 @@ const db = admin.firestore(); // GET: 全ての投稿を取得 router.get("/", async (req: Request, res: Response) => { try { + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); const goalSnapshot = await db.collection("goal").get(); if (goalSnapshot.empty) { @@ -37,16 +42,24 @@ router.get("/", async (req: Request, res: Response) => { return res.json(postData); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error fetching posts" }); } }); // GET: userIdから投稿を取得 router.get("/:userId", async (req: Request, res: Response) => { - const userId = req.params.userId; - try { + const userId = req.params.userId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); + const goalSnapshot = await db .collection("goal") .where("userId", "==", userId) @@ -73,12 +86,16 @@ router.get("/:userId", async (req: Request, res: Response) => { return res.json(postData); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error fetching user's posts" }); } }); -// POST: 新しい投稿を作成 +// POST: 投稿を作成・更新 router.post("/", async (req: Request, res: Response) => { let goalId: PostWithGoalId["goalId"]; let text: PostWithGoalId["text"]; @@ -87,8 +104,16 @@ router.post("/", async (req: Request, res: Response) => { try { ({ goalId, text = "", storedId, submittedAt } = req.body); + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(400).json({ message: "Invalid request body" }); } @@ -120,11 +145,21 @@ router.post("/", async (req: Request, res: Response) => { }, }); + logger.info({ + message: "Post updated successfully", + goalId, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res .status(201) - .json({ message: "Post created successfully", goalId }); + .json({ message: "Post updated successfully", goalId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error creating post" }); } }); @@ -133,6 +168,10 @@ router.post("/", async (req: Request, res: Response) => { router.delete("/:goalId", async (req: Request, res: Response) => { try { const goalId = req.params.goalId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); if (!goalId) { return res.status(400).json({ message: "goalId is required" }); @@ -154,7 +193,11 @@ router.delete("/:goalId", async (req: Request, res: Response) => { await file.delete(); logger.info("Image deleted successfully:", storedId); } catch (error) { - logger.error("Error deleting image:", error); + logger.error({ + error: `Error deleting image: ${error}`, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error deleting image" }); } } @@ -163,9 +206,19 @@ router.delete("/:goalId", async (req: Request, res: Response) => { post: null, }); + logger.info({ + message: "Post deleted successfully", + goalId, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.json({ message: "Post deleted successfully", goalId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error deleting post" }); } }); diff --git a/functions/src/routers/reactionRouter.ts b/functions/src/routers/reactionRouter.ts index 043733c..989696c 100644 --- a/functions/src/routers/reactionRouter.ts +++ b/functions/src/routers/reactionRouter.ts @@ -1,6 +1,7 @@ import express, { Request, Response } from "express"; import admin from "firebase-admin"; import { logger } from "firebase-functions"; +import { getHttpRequestData, getRequestData } from ".."; import { Reaction, ReactionTypeMap } from "../types"; const router = express.Router(); @@ -9,6 +10,10 @@ const db = admin.firestore(); // PUT: リアクションを更新 router.put("/:goalId", async (req: Request, res: Response) => { const goalId = req.params.goalId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); const { userId, reactionType }: Partial = req.body; if (!userId || !goalId) { @@ -50,9 +55,20 @@ router.put("/:goalId", async (req: Request, res: Response) => { reaction: currentReactions, }); + logger.info({ + message: "Reaction updated successfully", + goalId, + userId, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.json({ message: "Reaction updated successfully", goalId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error updating Reaction" }); } }); diff --git a/functions/src/routers/resultRouter.ts b/functions/src/routers/resultRouter.ts index 4c40183..e8e2a9d 100644 --- a/functions/src/routers/resultRouter.ts +++ b/functions/src/routers/resultRouter.ts @@ -1,6 +1,7 @@ import express, { Request, Response } from "express"; import admin from "firebase-admin"; import { logger } from "firebase-functions"; +import { getHttpRequestData, getRequestData } from ".."; import { countCompletedGoals, countFailedGoals, getStreak } from "../status"; import { GoalWithIdAndUserData, User } from "../types"; import { getUserFromId } from "./userRouter"; @@ -150,6 +151,10 @@ const processGoals = async ( // onlyFinishedの場合のlimitはsuccessをlimitの数返してその期間内のfailedを追加で返す router.get("/:userId?", async (req: Request, res: Response) => { const userId = req.params.userId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); let limit = parseInt(req.query.limit as string) || 10; if (limit < 1 || limit > 100) { @@ -170,7 +175,11 @@ router.get("/:userId?", async (req: Request, res: Response) => { ); return res.json(results); } catch (error) { - logger.info(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error fetching results" }); } }); diff --git a/functions/src/routers/userRouter.ts b/functions/src/routers/userRouter.ts index 25cb397..b06180a 100644 --- a/functions/src/routers/userRouter.ts +++ b/functions/src/routers/userRouter.ts @@ -1,6 +1,7 @@ import express, { Request, Response } from "express"; import admin from "firebase-admin"; import { logger } from "firebase-functions"; +import { getHttpRequestData, getRequestData } from ".."; import { countCompletedGoals, countFailedGoals, getStreak } from "../status"; import { User } from "../types"; @@ -10,6 +11,10 @@ const db = admin.firestore(); // GET: 全てのユーザーデータを取得 router.get("/", async (req: Request, res: Response) => { try { + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); const userSnapshot = await db.collection("user").get(); if (userSnapshot.empty) { @@ -36,16 +41,24 @@ router.get("/", async (req: Request, res: Response) => { return res.json(userData); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error fetching user data" }); } }); // GET: userIdからユーザー情報を取得 router.get("/id/:userId", async (req: Request, res: Response) => { - const userId = req.params.userId; - try { + const userId = req.params.userId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); + const userDoc = await getUserFromId(userId); if (!userDoc.exists) { @@ -67,7 +80,11 @@ router.get("/id/:userId", async (req: Request, res: Response) => { return res.json(userData); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error fetching user data" }); } }); @@ -80,8 +97,16 @@ router.post("/", async (req: Request, res: Response) => { try { ({ name, userId, fcmToken = "" } = req.body); + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(400).json({ message: "Invalid request body" }); } @@ -96,11 +121,22 @@ router.post("/", async (req: Request, res: Response) => { fcmToken: fcmToken, }); + logger.info({ + message: "User created successfully", + userId, + name, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res .status(201) .json({ message: "User created successfully", userId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error creating user" }); } }); @@ -108,6 +144,10 @@ router.post("/", async (req: Request, res: Response) => { // PUT: ユーザー情報を更新 router.put("/:userId", async (req: Request, res: Response) => { const userId = req.params.userId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); const { name, fcmToken }: Partial = req.body; if (!userId) { @@ -130,9 +170,19 @@ router.put("/:userId", async (req: Request, res: Response) => { try { await db.collection("user").doc(userId).update(updateData); + logger.info({ + message: "User updated successfully", + userId, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.json({ message: "User updated successfully", userId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error updating user" }); } }); @@ -141,6 +191,10 @@ router.put("/:userId", async (req: Request, res: Response) => { router.delete("/:userId", async (req: Request, res: Response) => { try { const userId = req.params.userId; + logger.info({ + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); if (!userId) { return res.status(400).json({ message: "userId is required" }); @@ -154,9 +208,19 @@ router.delete("/:userId", async (req: Request, res: Response) => { } await userRef.delete(); + logger.info({ + message: "User deleted successfully", + userId, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.json({ message: "User deleted successfully", userId }); } catch (error) { - logger.error(error); + logger.error({ + error, + httpRequest: getHttpRequestData(req), + requestLog: getRequestData(req), + }); return res.status(500).json({ message: "Error deleting user" }); } }); diff --git a/functions/src/tasks.ts b/functions/src/tasks.ts index c377552..33d8c20 100644 --- a/functions/src/tasks.ts +++ b/functions/src/tasks.ts @@ -21,12 +21,12 @@ export const createTasksOnGoalCreate = onDocumentCreated( } if (!projectId) { - logger.info("GCP_PROJECT_ID is not defined."); + logger.error("GCP_PROJECT_ID is not defined."); return; } if (!event.data) { - logger.info("No data found in event."); + logger.error("No data found in event."); return; } @@ -77,12 +77,12 @@ export const deleteTasksOnGoalDelete = onDocumentDeleted( } if (!projectId) { - logger.info("GCP_PROJECT_ID is not defined."); + logger.error("GCP_PROJECT_ID is not defined."); return; } if (!event.data) { - logger.info("No data found in event."); + logger.error("No data found in event."); return; } @@ -93,7 +93,7 @@ export const deleteTasksOnGoalDelete = onDocumentDeleted( await tasksClient.deleteTask({ name: taskName }); logger.info("Task deleted for goalId:", goalId); } catch (error) { - logger.info("Error deleting task:", error); + logger.error("Error deleting task:", error); } } ); @@ -110,12 +110,12 @@ export const deleteTasksOnPostCreate = onDocumentUpdated( } if (!projectId) { - logger.info("GCP_PROJECT_ID is not defined."); + logger.error("GCP_PROJECT_ID is not defined."); return; } if (!event.data) { - logger.info("No data found in event."); + logger.error("No data found in event."); return; } @@ -130,7 +130,7 @@ export const deleteTasksOnPostCreate = onDocumentUpdated( await tasksClient.deleteTask({ name: taskName }); logger.info("Task deleted for goalId:", goalId); } catch (error) { - logger.info("Error deleting task:", error); + logger.error("Error deleting task:", error); } } ); diff --git a/src/Components/GoalModal/CreateGoalModal.tsx b/src/Components/GoalModal/CreateGoalModal.tsx index 88830c9..768235a 100644 --- a/src/Components/GoalModal/CreateGoalModal.tsx +++ b/src/Components/GoalModal/CreateGoalModal.tsx @@ -137,7 +137,7 @@ export default function CreateGoalModal({ required /> - 期限が1時間以内の目標は削除できなくなります + 期限が1時間以内の目標は削除・編集できなくなります