From 797e19b167040403540e276400c74bf1474c78d3 Mon Sep 17 00:00:00 2001 From: nakaterm <104970808+nakaterm@users.noreply.github.com> Date: Mon, 8 Dec 2025 02:29:48 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=E5=85=B1=E9=80=9A=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=82=92=20main.ts=20=E3=81=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/main.ts | 13 ++++++++++--- server/src/routes/projects.ts | 12 +----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/server/src/main.ts b/server/src/main.ts index 1bf9278..ec54337 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -3,16 +3,23 @@ import { PrismaClient } from "@prisma/client"; import dotenv from "dotenv"; import { Hono } from "hono"; import { cors } from "hono/cors"; +import { customAlphabet } from "nanoid"; import { browserIdMiddleware } from "./middleware/browserId.js"; import projectsRoutes from "./routes/projects.js"; dotenv.config(); +/** + * ハイフン・アンダースコアを含まない Nano ID 形式。 + */ +export const nanoid = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 21); + export const prisma = new PrismaClient(); -const port = process.env.PORT || 3000; + +const port = Number(process.env.PORT) || 3000; const allowedOrigins = process.env.CORS_ALLOW_ORIGINS?.split(",") || []; -type AppVariables = { +export type AppVariables = { browserId: string; }; @@ -33,7 +40,7 @@ const app = new Hono<{ Variables: AppVariables }>() serve( { fetch: app.fetch, - port: Number(port), + port, hostname: "0.0.0.0", }, () => { diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index 948ce5d..178f807 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -1,24 +1,14 @@ import { zValidator } from "@hono/zod-validator"; import dotenv from "dotenv"; import { Hono } from "hono"; -import { customAlphabet } from "nanoid"; import { z } from "zod"; import { editReqSchema, projectReqSchema, submitReqSchema } from "../../../common/validators.js"; -import { prisma } from "../main.js"; +import { type AppVariables, nanoid, prisma } from "../main.js"; dotenv.config(); -/** - * ハイフン・アンダースコアを含まない Nano ID 形式。 - */ -const nanoid = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 21); - const projectIdParamsSchema = z.object({ projectId: z.string().length(21) }); -type AppVariables = { - browserId: string; -}; - const router = new Hono<{ Variables: AppVariables }>() // プロジェクト作成 .post("/", zValidator("json", projectReqSchema), async (c) => { From 7b3017b5eea1d8730b75220e0b4fd499f9f1ff0e Mon Sep 17 00:00:00 2001 From: nakaterm <104970808+nakaterm@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:31:27 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=92?= =?UTF-8?q?=E5=85=B1=E9=80=9A=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Project.tsx | 5 +- server/src/main.ts | 6 +- server/src/routes/projects.ts | 498 ++++++++++++++++------------------ 3 files changed, 240 insertions(+), 269 deletions(-) diff --git a/client/src/pages/Project.tsx b/client/src/pages/Project.tsx index 5b6be0d..b7230a4 100644 --- a/client/src/pages/Project.tsx +++ b/client/src/pages/Project.tsx @@ -93,6 +93,7 @@ export default function ProjectPage() { projectName: string; } | null>(null); + // TODO: グローバルにしないと、delete の際は遷移を伴うので表示されない const [toast, setToast] = useState<{ message: string; variant: "success" | "error"; @@ -215,9 +216,8 @@ export default function ProjectPage() { projectName: name, }); } else { - const { message } = await res.json(); setToast({ - message, + message: "イベントの作成に失敗しました。", variant: "error", }); setTimeout(() => setToast(null), 3000); @@ -604,6 +604,7 @@ export default function ProjectPage() { if (!res.ok) { throw new Error("削除に失敗しました。"); } + // TODO: トーストをグローバルにする navigate("/home"); setToast({ message: "イベントを削除しました。", diff --git a/server/src/main.ts b/server/src/main.ts index ec54337..8eb6867 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -35,7 +35,11 @@ const app = new Hono<{ Variables: AppVariables }>() .get("/", (c) => { return c.json({ message: "Hello! イツヒマ?" }); }) - .route("/projects", projectsRoutes); + .route("/projects", projectsRoutes) + .onError((err, c) => { + console.error(err); + return c.json({ message: "Internal Server Error" }, 500); + }); serve( { diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index 178f807..bf65b94 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -13,262 +13,238 @@ const router = new Hono<{ Variables: AppVariables }>() // プロジェクト作成 .post("/", zValidator("json", projectReqSchema), async (c) => { const browserId = c.get("browserId"); - try { - const data = c.req.valid("json"); - - const event = await prisma.project.create({ - data: { - id: nanoid(), - name: data.name, - description: data.description.trim() || null, - startDate: new Date(data.startDate), - endDate: new Date(data.endDate), - allowedRanges: { - create: data.allowedRanges.map((range) => ({ - startTime: new Date(range.startTime), - endTime: new Date(range.endTime), - })), - }, - hosts: { - create: { - browserId, - }, - }, - participationOptions: { - create: data.participationOptions.map((opt) => ({ - id: opt.id, - label: opt.label, - color: opt.color, - })), + const data = c.req.valid("json"); + + const event = await prisma.project.create({ + data: { + id: nanoid(), + name: data.name, + description: data.description.trim() || null, + startDate: new Date(data.startDate), + endDate: new Date(data.endDate), + allowedRanges: { + create: data.allowedRanges.map((range) => ({ + startTime: new Date(range.startTime), + endTime: new Date(range.endTime), + })), + }, + hosts: { + create: { + browserId, }, }, - include: { hosts: true, participationOptions: true }, - }); + participationOptions: { + create: data.participationOptions.map((opt) => ({ + id: opt.id, + label: opt.label, + color: opt.color, + })), + }, + }, + include: { hosts: true, participationOptions: true }, + }); - return c.json({ id: event.id, name: event.name }, 201); - } catch (_err) { - return c.json({ message: "イベント作成時にエラーが発生しました" }, 500); - } + return c.json({ id: event.id, name: event.name }, 201); }) // 自分が関連するプロジェクト取得 .get("/mine", async (c) => { const browserId = c.get("browserId"); - try { - const involvedProjects = await prisma.project.findMany({ - where: { - OR: [ - { hosts: { some: { browserId } } }, - { - guests: { - some: { browserId }, - }, - }, - ], - }, - select: { - id: true, - name: true, - description: true, - startDate: true, - endDate: true, - hosts: { - select: { - browserId: true, + const involvedProjects = await prisma.project.findMany({ + where: { + OR: [ + { hosts: { some: { browserId } } }, + { + guests: { + some: { browserId }, }, }, + ], + }, + select: { + id: true, + name: true, + description: true, + startDate: true, + endDate: true, + hosts: { + select: { + browserId: true, + }, }, - }); - - return c.json( - involvedProjects.map((p) => ({ - id: p.id, - name: p.name, - description: p.description ?? "", - startDate: p.startDate, - endDate: p.endDate, - isHost: p.hosts.some((host) => host.browserId === browserId), - })), - 200, - ); - } catch (error) { - console.error(error); - return c.json({ message: "エラーが発生しました。" }, 500); - } + }, + }); + + return c.json( + involvedProjects.map((p) => ({ + id: p.id, + name: p.name, + description: p.description ?? "", + startDate: p.startDate, + endDate: p.endDate, + isHost: p.hosts.some((host) => host.browserId === browserId), + })), + 200, + ); }) // プロジェクト取得 .get("/:projectId", zValidator("param", projectIdParamsSchema), async (c) => { const browserId = c.get("browserId"); - try { - const { projectId } = c.req.valid("param"); - const projectRow = await prisma.project.findUnique({ - where: { id: projectId }, - include: { - allowedRanges: true, - participationOptions: true, - guests: { - include: { - slots: true, // slots 全部欲しいなら select より include - }, + const { projectId } = c.req.valid("param"); + const projectRow = await prisma.project.findUnique({ + where: { id: projectId }, + include: { + allowedRanges: true, + participationOptions: true, + guests: { + include: { + slots: true, // slots 全部欲しいなら select より include }, - hosts: true, // 全部欲しいなら select 省略 }, - }); + hosts: true, // 全部欲しいなら select 省略 + }, + }); - if (!projectRow) { - return c.json({ message: "イベントが見つかりません。" }, 404); - } - - const data = { - ...projectRow, - description: projectRow.description ?? "", - hosts: projectRow.hosts.map((h) => { - const { browserId: _, ...rest } = h; - return rest; - }), - guests: projectRow.guests.map((g) => { - const { browserId: _, ...rest } = g; - return rest; - }), - isHost: projectRow.hosts.some((h) => h.browserId === browserId), - meAsGuest: projectRow.guests.find((g) => g.browserId === browserId) ?? null, - }; - return c.json(data, 200); - } catch (error) { - console.error("イベント取得エラー:", error); - return c.json({ message: "イベント取得中にエラーが発生しました。" }, 500); + if (!projectRow) { + return c.json({ message: "イベントが見つかりません。" }, 404); } + + const data = { + ...projectRow, + description: projectRow.description ?? "", + hosts: projectRow.hosts.map((h) => { + const { browserId: _, ...rest } = h; + return rest; + }), + guests: projectRow.guests.map((g) => { + const { browserId: _, ...rest } = g; + return rest; + }), + isHost: projectRow.hosts.some((h) => h.browserId === browserId), + meAsGuest: projectRow.guests.find((g) => g.browserId === browserId) ?? null, + }; + return c.json(data, 200); }) // プロジェクト編集 .put("/:projectId", zValidator("param", projectIdParamsSchema), zValidator("json", editReqSchema), async (c) => { const browserId = c.get("browserId"); - try { - const { projectId } = c.req.valid("param"); - const data = c.req.valid("json"); + const { projectId } = c.req.valid("param"); + const data = c.req.valid("json"); - // ホスト認証とゲスト存在確認を一括取得 - const [host, existingGuest] = await Promise.all([ - prisma.host.findFirst({ - where: { - browserId, - projectId: projectId, - }, - }), - prisma.guest.findFirst({ - where: { projectId: projectId }, - }), - ]); + // ホスト認証とゲスト存在確認を一括取得 + const [host, existingGuest] = await Promise.all([ + prisma.host.findFirst({ + where: { + browserId, + projectId: projectId, + }, + }), + prisma.guest.findFirst({ + where: { projectId: projectId }, + }), + ]); + + // ホストが存在しなければ403 + if (!host) { + return c.json({ message: "アクセス権限がありません。" }, 403); + } - // ホストが存在しなければ403 - if (!host) { - return c.json({ message: "アクセス権限がありません。" }, 403); + // 参加形態の更新 + if (data.participationOptions) { + // 最低1つの参加形態が必要 + if (data.participationOptions.length === 0) { + return c.json({ message: "参加形態は最低1つ必要です。" }, 400); } - // 参加形態の更新 - if (data.participationOptions) { - // 最低1つの参加形態が必要 - if (data.participationOptions.length === 0) { - return c.json({ message: "参加形態は最低1つ必要です。" }, 400); - } - - // 削除対象の参加形態に Slot が紐づいているかチェック - const existingOptions = await prisma.participationOption.findMany({ - where: { projectId }, - include: { slots: { select: { id: true } } }, - }); - - const newOptionIds = data.participationOptions.map((o) => o.id); - const optionsToDelete = existingOptions.filter((o) => !newOptionIds.includes(o.id)); - const undeletableOptions = optionsToDelete.filter((o) => o.slots.length > 0); - - if (undeletableOptions.length > 0) { - const labels = undeletableOptions.map((o) => o.label).join(", "); - return c.json( - { - message: `以下の参加形態は日程が登録されているため削除できません: ${labels}`, - }, - 400, - ); - } - - await prisma.$transaction([ - // 既存の参加形態で、新しいリストにないものを削除 - prisma.participationOption.deleteMany({ - where: { - projectId, - id: { - notIn: newOptionIds, - }, + // 削除対象の参加形態に Slot が紐づいているかチェック + const existingOptions = await prisma.participationOption.findMany({ + where: { projectId }, + include: { slots: { select: { id: true } } }, + }); + + const newOptionIds = data.participationOptions.map((o) => o.id); + const optionsToDelete = existingOptions.filter((o) => !newOptionIds.includes(o.id)); + const undeletableOptions = optionsToDelete.filter((o) => o.slots.length > 0); + + if (undeletableOptions.length > 0) { + const labels = undeletableOptions.map((o) => o.label).join(", "); + return c.json( + { + message: `以下の参加形態は日程が登録されているため削除できません: ${labels}`, + }, + 400, + ); + } + + await prisma.$transaction([ + // 既存の参加形態で、新しいリストにないものを削除 + prisma.participationOption.deleteMany({ + where: { + projectId, + id: { + notIn: newOptionIds, }, + }, + }), + // 既存の参加形態を更新または新規作成 + ...data.participationOptions.map((opt) => + prisma.participationOption.upsert({ + where: { id: opt.id }, + update: { label: opt.label, color: opt.color }, + create: { id: opt.id, label: opt.label, color: opt.color, projectId }, }), - // 既存の参加形態を更新または新規作成 - ...data.participationOptions.map((opt) => - prisma.participationOption.upsert({ - where: { id: opt.id }, - update: { label: opt.label, color: opt.color }, - create: { id: opt.id, label: opt.label, color: opt.color, projectId }, - }), - ), - ]); - } + ), + ]); + } - // 更新処理 - const updatedEvent = await prisma.project.update({ - where: { id: projectId }, - data: existingGuest - ? { - name: data.name, - description: data.description?.trim() || null, - } // ゲストがいれば名前と説明だけ - : { - name: data.name, - description: data.description?.trim() || null, - startDate: data.startDate ? new Date(data.startDate) : undefined, - endDate: data.endDate ? new Date(data.endDate) : undefined, - allowedRanges: { - deleteMany: {}, // 既存削除 - create: data.allowedRanges?.map((r) => ({ - startTime: new Date(r.startTime), - endTime: new Date(r.endTime), - })), - }, + // 更新処理 + const updatedEvent = await prisma.project.update({ + where: { id: projectId }, + data: existingGuest + ? { + name: data.name, + description: data.description?.trim() || null, + } // ゲストがいれば名前と説明だけ + : { + name: data.name, + description: data.description?.trim() || null, + startDate: data.startDate ? new Date(data.startDate) : undefined, + endDate: data.endDate ? new Date(data.endDate) : undefined, + allowedRanges: { + deleteMany: {}, // 既存削除 + create: data.allowedRanges?.map((r) => ({ + startTime: new Date(r.startTime), + endTime: new Date(r.endTime), + })), }, - include: { allowedRanges: true, participationOptions: true }, - }); + }, + include: { allowedRanges: true, participationOptions: true }, + }); - return c.json({ event: updatedEvent }, 200); - } catch (error) { - console.error("イベント更新エラー:", error); - return c.json({ message: "イベント更新中にエラーが発生しました。" }, 500); - } + return c.json({ event: updatedEvent }, 200); }) // プロジェクト削除 .delete("/:projectId", zValidator("param", projectIdParamsSchema), async (c) => { const browserId = c.get("browserId"); - try { - const { projectId } = c.req.valid("param"); - // Host 認証 - const host = await prisma.host.findFirst({ - where: { projectId, browserId }, - }); - - if (!host) { - return c.json({ message: "削除権限がありません。" }, 403); - } - // 関連データを削除(Cascade を使っていない場合) - await prisma.project.delete({ - where: { id: projectId }, - }); - return c.json({ message: "イベントを削除しました。" }, 200); - } catch (error) { - console.error("イベント削除エラー:", error); - return c.json({ message: "イベント削除中にエラーが発生しました。" }, 500); + const { projectId } = c.req.valid("param"); + // Host 認証 + const host = await prisma.host.findFirst({ + where: { projectId, browserId }, + }); + + if (!host) { + return c.json({ message: "削除権限がありません。" }, 403); } + // 関連データを削除(Cascade を使っていない場合) + await prisma.project.delete({ + where: { id: projectId }, + }); + return c.json({ message: "イベントを削除しました。" }, 200); }) // 日程の提出。 @@ -289,28 +265,23 @@ const router = new Hono<{ Variables: AppVariables }>() const { name, slots } = c.req.valid("json"); - try { - await prisma.guest.create({ - data: { - name, - browserId, - project: { connect: { id: projectId } }, - slots: { - create: slots?.map((slot) => ({ - from: slot.start, - to: slot.end, - projectId, - participationOptionId: slot.participationOptionId, - })), - }, + await prisma.guest.create({ + data: { + name, + browserId, + project: { connect: { id: projectId } }, + slots: { + create: slots?.map((slot) => ({ + from: slot.start, + to: slot.end, + projectId, + participationOptionId: slot.participationOptionId, + })), }, - include: { slots: true }, - }); - return c.json("日時が登録されました!", 201); - } catch (error) { - console.error("登録エラー:", error); - return c.json({ message: "サーバーエラーが発生しました" }, 500); - } + }, + include: { slots: true }, + }); + return c.json("日時が登録されました!", 201); }, ) @@ -325,39 +296,34 @@ const router = new Hono<{ Variables: AppVariables }>() const { name, slots } = c.req.valid("json"); - try { - const existingGuest = await prisma.guest.findFirst({ - where: { projectId, browserId }, - include: { slots: true }, - }); - - if (!existingGuest) { - return c.json({ message: "ゲスト情報が見つかりません。" }, 404); - } - const slotData = slots?.map((slot) => ({ - from: slot.start, - to: slot.end, - projectId, - participationOptionId: slot.participationOptionId, - })); - - await prisma.slot.deleteMany({ where: { guestId: existingGuest.id } }); - - // ゲスト情報更新 - const guest = await prisma.guest.update({ - where: { id: existingGuest.id }, - data: { - slots: { create: slotData }, - name, - }, - include: { slots: true }, - }); + const existingGuest = await prisma.guest.findFirst({ + where: { projectId, browserId }, + include: { slots: true }, + }); - return c.json({ message: "ゲスト情報が更新されました!", guest }, 200); - } catch (error) { - console.error("処理中のエラー:", error); - return c.json({ message: "サーバーエラーが発生しました" }, 500); + if (!existingGuest) { + return c.json({ message: "ゲスト情報が見つかりません。" }, 404); } + const slotData = slots?.map((slot) => ({ + from: slot.start, + to: slot.end, + projectId, + participationOptionId: slot.participationOptionId, + })); + + await prisma.slot.deleteMany({ where: { guestId: existingGuest.id } }); + + // ゲスト情報更新 + const guest = await prisma.guest.update({ + where: { id: existingGuest.id }, + data: { + slots: { create: slotData }, + name, + }, + include: { slots: true }, + }); + + return c.json({ message: "ゲスト情報が更新されました!", guest }, 200); }, ); From 4eb8c6003622b192667ab4e4a2f89f5fef7909d6 Mon Sep 17 00:00:00 2001 From: nakaterm <104970808+nakaterm@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:31:50 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20Prisma=20=E3=81=A7=E3=81=AE?= =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=BF=E5=8F=96=E5=BE=97=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/routes/projects.ts | 163 +++++++++++++++++----------------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index bf65b94..4938a80 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -13,17 +13,17 @@ const router = new Hono<{ Variables: AppVariables }>() // プロジェクト作成 .post("/", zValidator("json", projectReqSchema), async (c) => { const browserId = c.get("browserId"); - const data = c.req.valid("json"); + const input = c.req.valid("json"); - const event = await prisma.project.create({ + const project = await prisma.project.create({ data: { id: nanoid(), - name: data.name, - description: data.description.trim() || null, - startDate: new Date(data.startDate), - endDate: new Date(data.endDate), + name: input.name, + description: input.description.trim() || null, + startDate: new Date(input.startDate), + endDate: new Date(input.endDate), allowedRanges: { - create: data.allowedRanges.map((range) => ({ + create: input.allowedRanges.map((range) => ({ startTime: new Date(range.startTime), endTime: new Date(range.endTime), })), @@ -34,24 +34,27 @@ const router = new Hono<{ Variables: AppVariables }>() }, }, participationOptions: { - create: data.participationOptions.map((opt) => ({ + create: input.participationOptions.map((opt) => ({ id: opt.id, label: opt.label, color: opt.color, })), }, }, - include: { hosts: true, participationOptions: true }, + select: { + id: true, + name: true, + }, }); - return c.json({ id: event.id, name: event.name }, 201); + return c.json({ id: project.id, name: project.name }, 201); }) // 自分が関連するプロジェクト取得 .get("/mine", async (c) => { const browserId = c.get("browserId"); - const involvedProjects = await prisma.project.findMany({ + const projects = await prisma.project.findMany({ where: { OR: [ { hosts: { some: { browserId } } }, @@ -62,22 +65,15 @@ const router = new Hono<{ Variables: AppVariables }>() }, ], }, - select: { - id: true, - name: true, - description: true, - startDate: true, - endDate: true, + include: { hosts: { - select: { - browserId: true, - }, + select: { browserId: true }, }, }, }); return c.json( - involvedProjects.map((p) => ({ + projects.map((p) => ({ id: p.id, name: p.name, description: p.description ?? "", @@ -92,50 +88,53 @@ const router = new Hono<{ Variables: AppVariables }>() // プロジェクト取得 .get("/:projectId", zValidator("param", projectIdParamsSchema), async (c) => { const browserId = c.get("browserId"); - const { projectId } = c.req.valid("param"); - const projectRow = await prisma.project.findUnique({ + + const project = await prisma.project.findUnique({ where: { id: projectId }, include: { allowedRanges: true, participationOptions: true, guests: { include: { - slots: true, // slots 全部欲しいなら select より include + slots: true, }, }, - hosts: true, // 全部欲しいなら select 省略 + hosts: true, }, }); - if (!projectRow) { + if (!project) { return c.json({ message: "イベントが見つかりません。" }, 404); } - const data = { - ...projectRow, - description: projectRow.description ?? "", - hosts: projectRow.hosts.map((h) => { - const { browserId: _, ...rest } = h; - return rest; - }), - guests: projectRow.guests.map((g) => { - const { browserId: _, ...rest } = g; - return rest; - }), - isHost: projectRow.hosts.some((h) => h.browserId === browserId), - meAsGuest: projectRow.guests.find((g) => g.browserId === browserId) ?? null, - }; - return c.json(data, 200); + const guest = project.guests.find((g) => g.browserId === browserId); + const meAsGuest = guest ? (({ browserId, ...rest }) => rest)(guest) : null; + + return c.json( + { + id: project.id, + name: project.name, + description: project.description ?? "", + startDate: project.startDate, + endDate: project.endDate, + allowedRanges: project.allowedRanges, + participationOptions: project.participationOptions, + hosts: project.hosts.map(({ browserId, ...rest }) => rest), + guests: project.guests.map(({ browserId, ...rest }) => rest), + isHost: project.hosts.some((h) => h.browserId === browserId), + meAsGuest, + }, + 200, + ); }) // プロジェクト編集 .put("/:projectId", zValidator("param", projectIdParamsSchema), zValidator("json", editReqSchema), async (c) => { const browserId = c.get("browserId"); const { projectId } = c.req.valid("param"); - const data = c.req.valid("json"); + const input = c.req.valid("json"); - // ホスト認証とゲスト存在確認を一括取得 const [host, existingGuest] = await Promise.all([ prisma.host.findFirst({ where: { @@ -148,15 +147,13 @@ const router = new Hono<{ Variables: AppVariables }>() }), ]); - // ホストが存在しなければ403 if (!host) { return c.json({ message: "アクセス権限がありません。" }, 403); } // 参加形態の更新 - if (data.participationOptions) { - // 最低1つの参加形態が必要 - if (data.participationOptions.length === 0) { + if (input.participationOptions) { + if (input.participationOptions.length === 0) { return c.json({ message: "参加形態は最低1つ必要です。" }, 400); } @@ -165,11 +162,9 @@ const router = new Hono<{ Variables: AppVariables }>() where: { projectId }, include: { slots: { select: { id: true } } }, }); - - const newOptionIds = data.participationOptions.map((o) => o.id); + const newOptionIds = input.participationOptions.map((o) => o.id); const optionsToDelete = existingOptions.filter((o) => !newOptionIds.includes(o.id)); const undeletableOptions = optionsToDelete.filter((o) => o.slots.length > 0); - if (undeletableOptions.length > 0) { const labels = undeletableOptions.map((o) => o.label).join(", "); return c.json( @@ -191,7 +186,7 @@ const router = new Hono<{ Variables: AppVariables }>() }, }), // 既存の参加形態を更新または新規作成 - ...data.participationOptions.map((opt) => + ...input.participationOptions.map((opt) => prisma.participationOption.upsert({ where: { id: opt.id }, update: { label: opt.label, color: opt.color }, @@ -201,22 +196,21 @@ const router = new Hono<{ Variables: AppVariables }>() ]); } - // 更新処理 - const updatedEvent = await prisma.project.update({ + const updatedProject = await prisma.project.update({ where: { id: projectId }, data: existingGuest ? { - name: data.name, - description: data.description?.trim() || null, - } // ゲストがいれば名前と説明だけ + name: input.name, + description: input.description?.trim() || null, + } : { - name: data.name, - description: data.description?.trim() || null, - startDate: data.startDate ? new Date(data.startDate) : undefined, - endDate: data.endDate ? new Date(data.endDate) : undefined, + name: input.name, + description: input.description?.trim() || null, + startDate: input.startDate ? new Date(input.startDate) : undefined, + endDate: input.endDate ? new Date(input.endDate) : undefined, allowedRanges: { deleteMany: {}, // 既存削除 - create: data.allowedRanges?.map((r) => ({ + create: input.allowedRanges?.map((r) => ({ startTime: new Date(r.startTime), endTime: new Date(r.endTime), })), @@ -225,26 +219,31 @@ const router = new Hono<{ Variables: AppVariables }>() include: { allowedRanges: true, participationOptions: true }, }); - return c.json({ event: updatedEvent }, 200); + return c.json({ event: updatedProject }, 200); }) // プロジェクト削除 .delete("/:projectId", zValidator("param", projectIdParamsSchema), async (c) => { const browserId = c.get("browserId"); const { projectId } = c.req.valid("param"); - // Host 認証 - const host = await prisma.host.findFirst({ - where: { projectId, browserId }, + + const host = await prisma.host.findUnique({ + where: { + browserId_projectId: { + browserId, + projectId, + }, + }, }); if (!host) { return c.json({ message: "削除権限がありません。" }, 403); } - // 関連データを削除(Cascade を使っていない場合) + await prisma.project.delete({ where: { id: projectId }, }); - return c.json({ message: "イベントを削除しました。" }, 200); + return c.json(204); }) // 日程の提出。 @@ -253,18 +252,22 @@ const router = new Hono<{ Variables: AppVariables }>() zValidator("param", projectIdParamsSchema), zValidator("json", submitReqSchema), async (c) => { - const { projectId } = c.req.valid("param"); const browserId = c.get("browserId"); + const { projectId } = c.req.valid("param"); + const { name, slots } = c.req.valid("json"); - const existingGuest = await prisma.guest.findFirst({ - where: { projectId, browserId }, + const existingGuest = await prisma.guest.findUnique({ + where: { + browserId_projectId: { + browserId, + projectId, + }, + }, }); if (existingGuest) { - return c.json({ message: "すでに登録済みです" }, 403); + return c.json({ message: "提出済みです。" }, 403); } - const { name, slots } = c.req.valid("json"); - await prisma.guest.create({ data: { name, @@ -281,7 +284,7 @@ const router = new Hono<{ Variables: AppVariables }>() }, include: { slots: true }, }); - return c.json("日時が登録されました!", 201); + return c.json("日程が提出されました。", 201); }, ) @@ -291,18 +294,17 @@ const router = new Hono<{ Variables: AppVariables }>() zValidator("param", projectIdParamsSchema), zValidator("json", submitReqSchema), async (c) => { - const { projectId } = c.req.valid("param"); const browserId = c.get("browserId"); - + const { projectId } = c.req.valid("param"); const { name, slots } = c.req.valid("json"); - const existingGuest = await prisma.guest.findFirst({ - where: { projectId, browserId }, + const existingGuest = await prisma.guest.findUnique({ + where: { browserId_projectId: { browserId, projectId } }, include: { slots: true }, }); if (!existingGuest) { - return c.json({ message: "ゲスト情報が見つかりません。" }, 404); + return c.json({ message: "既存の日程が見つかりません。" }, 404); } const slotData = slots?.map((slot) => ({ from: slot.start, @@ -313,7 +315,6 @@ const router = new Hono<{ Variables: AppVariables }>() await prisma.slot.deleteMany({ where: { guestId: existingGuest.id } }); - // ゲスト情報更新 const guest = await prisma.guest.update({ where: { id: existingGuest.id }, data: { @@ -323,7 +324,7 @@ const router = new Hono<{ Variables: AppVariables }>() include: { slots: true }, }); - return c.json({ message: "ゲスト情報が更新されました!", guest }, 200); + return c.json({ message: "日程が更新されました。", guest }, 200); }, ); From d86c4417417cec6f97466011159953605a2b2d91 Mon Sep 17 00:00:00 2001 From: nakaterm <104970808+nakaterm@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:41:53 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20middleware=20=E3=81=AE=E3=83=AC?= =?UTF-8?q?=E3=82=B9=E3=83=9D=E3=83=B3=E3=82=B9=E3=81=AF=E4=B9=97=E3=82=89?= =?UTF-8?q?=E3=81=AA=E3=81=84=E3=81=AE=E3=81=A7=E5=9B=9E=E9=81=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Home.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 258b0f9..0430380 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -31,8 +31,9 @@ export default function HomePage() { let errorMessage = "イベントの取得に失敗しました。"; try { const data = await res.json(); - if (data && typeof data.message === "string" && data.message.trim()) { - errorMessage = data.message.trim(); + const err = data as unknown as { message: string }; // Middleware のレスポンスは Hono RPC の型に乗らない + if (typeof err.message === "string" && err.message.trim()) { + errorMessage = err.message.trim(); } } catch (_) { // レスポンスがJSONでない場合は無視