From 60bb6935c984ea68cdbd5a6d07750ad7a0ceea17 Mon Sep 17 00:00:00 2001 From: system_master Date: Sat, 24 Jan 2026 19:18:54 +0900 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=97=A5=E3=81=A7=E6=99=82=E5=88=BB?= =?UTF-8?q?=E3=81=8C=E8=A2=AB=E3=82=89=E3=81=AA=E3=81=84=E9=96=8B=E5=82=AC?= =?UTF-8?q?=E3=82=92=E7=99=BB=E9=8C=B2=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/app/api/reserve-setting/[id]/route.ts | 312 +++++++++++----------- src/app/api/reserve-setting/route.ts | 46 ++-- 3 files changed, 186 insertions(+), 174 deletions(-) diff --git a/package.json b/package.json index 3e8813f..e2aca66 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev -p 3004", - "build": "next build", + "build": "dotenv -e .env.production -- next build", "start": "dotenv -e .env.production -- next start", "lint": "eslint" }, diff --git a/src/app/api/reserve-setting/[id]/route.ts b/src/app/api/reserve-setting/[id]/route.ts index 62e1d80..24e03d1 100644 --- a/src/app/api/reserve-setting/[id]/route.ts +++ b/src/app/api/reserve-setting/[id]/route.ts @@ -1,186 +1,184 @@ // src/app/api/reserve-setting/[id]/route.ts - import { NextResponse, type NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; import dayjs from "dayjs"; import { validateReserveSetting } from "@/lib/validators/reserveSettingValidator"; -export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { - try { - const { id } = await params; - const settingId = parseInt(id, 10); +type RouteContext = { params: Promise<{ id: string }> }; - const setting = await prisma.reserveSetting.findUnique({ - where: { autonum: settingId }, - select: { - autonum: true, - diner_date: true, - start_time: true, - end_time: true, - time_range: true, - limit_method: true, - limit_type: true, - reserve_limit: true, - }, - }); +export async function GET(req: NextRequest, context: RouteContext) { + try { + const { id } = await context.params; + const settingId = parseInt(id, 10); - if (!setting) { - return NextResponse.json({ error: "開催が見つかりません" }, { status: 404 }); - } + const setting = await prisma.reserveSetting.findUnique({ + where: { autonum: settingId }, + select: { + autonum: true, + diner_date: true, + start_time: true, + end_time: true, + time_range: true, + limit_method: true, + limit_type: true, + reserve_limit: true, + }, + }); - // 予約数集計 - let allCount = 0; - const blockCounts: Record = {}; + if (!setting) { + return NextResponse.json({ error: "開催が見つかりません" }, { status: 404 }); + } - if (setting.limit_method === "all") { - if (setting.limit_type === "set") { - allCount = await prisma.reserveCustomer.count({ - where: { reserve_num: settingId, cancel_flag: 0 }, - }); - } else { - const agg = await prisma.reserveCustomer.aggregate({ - where: { reserve_num: settingId, cancel_flag: 0 }, - _sum: { cust_parent: true, cust_child: true }, - }); - allCount = (agg._sum.cust_parent ?? 0) + (agg._sum.cust_child ?? 0); - } - } else { - if (setting.limit_type === "set") { - const rows = await prisma.reserveCustomer.groupBy({ - by: ["reserve_time"], - where: { reserve_num: settingId, cancel_flag: 0 }, - _count: { _all: true }, - }); - rows.forEach((r) => (blockCounts[r.reserve_time] = r._count._all)); - } else { - const rows = await prisma.reserveCustomer.groupBy({ - by: ["reserve_time"], - where: { reserve_num: settingId, cancel_flag: 0 }, - _sum: { cust_parent: true, cust_child: true }, - }); - rows.forEach((r) => { - blockCounts[r.reserve_time] = - (r._sum.cust_parent ?? 0) + (r._sum.cust_child ?? 0); - }); - } - } + let allCount = 0; + const blockCounts: Record = {}; - return NextResponse.json({ - ...setting, - current: { all: allCount, blocks: blockCounts }, - }); - } catch (error) { - console.error("GET Error:", error); - return NextResponse.json({ error: "取得に失敗しました" }, { status: 500 }); - } + if (setting.limit_method === "all") { + if (setting.limit_type === "set") { + allCount = await prisma.reserveCustomer.count({ + where: { reserve_num: settingId, cancel_flag: 0 }, + }); + } else { + const agg = await prisma.reserveCustomer.aggregate({ + where: { reserve_num: settingId, cancel_flag: 0 }, + _sum: { cust_parent: true, cust_child: true }, + }); + allCount = (agg._sum.cust_parent ?? 0) + (agg._sum.cust_child ?? 0); + } + } else { + if (setting.limit_type === "set") { + const rows = await prisma.reserveCustomer.groupBy({ + by: ["reserve_time"], + where: { reserve_num: settingId, cancel_flag: 0 }, + _count: { _all: true }, + }); + rows.forEach((r) => (blockCounts[r.reserve_time] = r._count._all)); + } else { + const rows = await prisma.reserveCustomer.groupBy({ + by: ["reserve_time"], + where: { reserve_num: settingId, cancel_flag: 0 }, + _sum: { cust_parent: true, cust_child: true }, + }); + rows.forEach((r) => { + blockCounts[r.reserve_time] = + (r._sum.cust_parent ?? 0) + (r._sum.cust_child ?? 0); + }); + } + } + + return NextResponse.json({ + ...setting, + current: { all: allCount, blocks: blockCounts }, + }); + } catch (error) { + console.error("GET Error:", error); + return NextResponse.json({ error: "取得に失敗しました" }, { status: 500 }); + } } -export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { - try { - const { id } = await params; - const settingId = Number(id); - const data = await req.json(); +export async function PUT(req: NextRequest, context: RouteContext) { + try { + const { id } = await context.params; + const settingId = parseInt(id, 10); + const data = await req.json(); - // 必須チェック - if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) { - return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 }); - } + if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) { + return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 }); + } - const dinerDate = dayjs(data.dinerDate); - if (dinerDate.isBefore(dayjs())) { - return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 }); - } + const now = dayjs(); + const dinerDate = dayjs(data.dinerDate); + if (dinerDate.isBefore(now)) { + return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 }); + } - // 開催日重複チェック(自分以外) - const checkDate = dinerDate.format("YYYY-MM-DD"); + // 同日開催OK(時間重複はNG) + const dinerDateStr = dinerDate.format("YYYY-MM-DD"); + const newStartTime = dayjs(data.dinerDate).format("HH:mm:ss"); + const newEndTime = dayjs(`${data.dinerDate.split("T")[0]}T${data.endTime}`).format("HH:mm:ss"); - const exists = await prisma.$queryRawUnsafe<{ dummy: number }[]>( - `SELECT 1 as dummy FROM reserve_setting WHERE diner_date = ? AND delete_flag = 0 AND autonum <> ? LIMIT 1`, - checkDate, - data.id - ); - console.log([checkDate, data.id, exists]); - if (exists.length > 0) { - return NextResponse.json( - { error: "同じ開催日が既に登録されています" }, - { status: 400 } - ); - } + const overlaps = await prisma.$queryRawUnsafe<{ dummy: number }[]>( + ` + SELECT 1 as dummy + FROM reserve_setting + WHERE diner_date = ? + AND delete_flag = 0 + AND autonum <> ? + AND start_time < ? + AND end_time > ? + LIMIT 1 + `, + dinerDateStr, + settingId, + newEndTime, + newStartTime + ); - // 共通バリデーション - const validationError = validateReserveSetting(data); - if (validationError) { - return NextResponse.json({ error: validationError }, { status: 400 }); - } + if (overlaps.length > 0) { + return NextResponse.json( + { error: "同じ開催日で時間が重複する開催が既に登録されています" }, + { status: 400 } + ); + } - // JST文字列に変換 - const dinerDateStr = dayjs(data.dinerDate).format("YYYY-MM-DD"); - const startTimeStr = dayjs(data.dinerDate).format("HH:mm:ss"); - const endTimeStr = dayjs(`${data.dinerDate.split("T")[0]}T${data.endTime}`).format( - "HH:mm:ss" - ); - const reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss"); - const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss"); + const validationError = validateReserveSetting(data); + if (validationError) { + return NextResponse.json({ error: validationError }, { status: 400 }); + } - // Raw SQL UPDATE - await prisma.$executeRawUnsafe( - `UPDATE reserve_setting + const reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss"); + const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss"); + + await prisma.$executeRawUnsafe( + `UPDATE reserve_setting SET diner_date = ?, start_time = ?, end_time = ?, time_range = ?, reserve_startdate = ?, reserve_limitdate = ?, parent_money = ?, child_money = ?, limit_method = ?, limit_type = ?, reserve_limit = ?, note = ?, lastupdate = NOW() WHERE autonum = ?`, - dinerDateStr, - startTimeStr, - endTimeStr, - Number(data.timeRange), - reserveStartStr, - reserveLimitStr, - Number(data.priceAdult), - Number(data.priceChild), - data.limitMethod, - data.limitUnit, - Number(data.limitValue), - data.note || null, - settingId - ); + dinerDateStr, + newStartTime, + newEndTime, + Number(data.timeRange), + reserveStartStr, + reserveLimitStr, + Number(data.priceAdult), + Number(data.priceChild), + data.limitMethod, + data.limitUnit, + Number(data.limitValue), + data.note || null, + settingId + ); - return NextResponse.json({ success: true }, { status: 200 }); - } catch (error) { - console.error("PUT Error:", error); - return NextResponse.json({ error: "更新に失敗しました" }, { status: 500 }); - } + return NextResponse.json({ success: true }, { status: 200 }); + } catch (error) { + console.error("PUT Error:", error); + return NextResponse.json({ error: "更新に失敗しました" }, { status: 500 }); + } } -interface DeleteExists { - autonum: number; -} -// DELETE: 論理削除(Raw SQL版) -export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { - try { - const { id } = await params; - const settingId = parseInt(id, 10); - - // 存在確認 - const rows = (await prisma.$queryRawUnsafe( - `SELECT autonum FROM reserve_setting WHERE autonum = ? AND delete_flag = 0 LIMIT 1`, - settingId - )) as DeleteExists[]; - if (rows.length === 0) { - return NextResponse.json({ error: "対象データが存在しません" }, { status: 404 }); - } - - // 論理削除 - await prisma.$executeRawUnsafe( - `UPDATE reserve_setting - SET delete_flag = 1, lastupdate = NOW() - WHERE autonum = ?`, - settingId - ); - - return NextResponse.json({ success: true }, { status: 200 }); - } catch (error) { - console.error("DELETE Error:", error); - return NextResponse.json({ error: "削除に失敗しました" }, { status: 500 }); - } +export async function DELETE(req: NextRequest, context: RouteContext) { + try { + const { id } = await context.params; + const settingId = parseInt(id, 10); + + const rows = (await prisma.$queryRawUnsafe( + `SELECT autonum FROM reserve_setting WHERE autonum = ? AND delete_flag = 0 LIMIT 1`, + settingId + )) as { autonum: number }[]; + + if (rows.length === 0) { + return NextResponse.json({ error: "対象データが存在しません" }, { status: 404 }); + } + + await prisma.$executeRawUnsafe( + `UPDATE reserve_setting SET delete_flag = 1, lastupdate = NOW() WHERE autonum = ?`, + settingId + ); + + return NextResponse.json({ success: true }, { status: 200 }); + } catch (error) { + console.error("DELETE Error:", error); + return NextResponse.json({ error: "削除に失敗しました" }, { status: 500 }); + } } diff --git a/src/app/api/reserve-setting/route.ts b/src/app/api/reserve-setting/route.ts index a84fecc..092eb94 100644 --- a/src/app/api/reserve-setting/route.ts +++ b/src/app/api/reserve-setting/route.ts @@ -72,8 +72,6 @@ export async function GET() { diner_range: `${dayjs(row.diner_date).format("M/DD")}\n${startHM}~${endHM}`, end_hm: endHM, - // ここは SQL 側で '%Y-%m-%d %H:%i:%s' の文字列にして返しているので - // その文字列を dayjs のフォーマットで ISO 風に成形 reserve_start: row.reserve_start_str ? dayjs(row.reserve_start_str, "YYYY-MM-DD HH:mm:ss").format("YYYY-MM-DDTHH:mm") : "", @@ -106,38 +104,30 @@ export async function GET() { } } -// POST: 新規作成 +// POST: 新規作成(同日でも時間が重ならなければOK) export async function POST(req: NextRequest) { try { const data = await req.json(); - console.log("受信:", data.timeRange); // 必須チェック if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) { return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 }); } + // 過去日禁止 const now = dayjs(); const dinerDate = dayjs(data.dinerDate); if (dinerDate.isBefore(now)) { return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 }); } - // --- 同日開催禁止(Raw SQL)--- - const checkDate = dinerDate.format("YYYY-MM-DD"); - const exists = await prisma.$queryRawUnsafe<{ dummy: number }[]>( - `SELECT 1 as dummy FROM reserve_setting WHERE diner_date = ? AND delete_flag = 0 LIMIT 1`, - checkDate - ); - if (exists.length > 0) { - return NextResponse.json({ error: "同じ開催日で既に登録があります" }, { status: 400 }); - } - // 追加バリデーション + + // 追加バリデーション(形式・範囲など) const validationError = validateReserveSetting(data); if (validationError) { return NextResponse.json({ error: validationError }, { status: 400 }); } - // JST文字列に変換 + // JST文字列に変換(※時間重複チェックでも使う) const dinerDateStr = dayjs(data.dinerDate).format("YYYY-MM-DD"); const startTimeStr = dayjs(data.dinerDate).format("HH:mm:ss"); const endTimeStr = dayjs(`${data.dinerDate.split("T")[0]}T${data.endTime}`).format( @@ -146,6 +136,30 @@ export async function POST(req: NextRequest) { const reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss"); const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss"); + // --- 同日・時間重複チェック --- + // 重なり判定: existingStart < newEnd AND existingEnd > newStart + const overlap = await prisma.$queryRawUnsafe<{ dummy: number }[]>( + ` + SELECT 1 as dummy + FROM reserve_setting + WHERE diner_date = ? + AND delete_flag = 0 + AND start_time < ? + AND end_time > ? + LIMIT 1 + `, + dinerDateStr, + endTimeStr, + startTimeStr + ); + + if (overlap.length > 0) { + return NextResponse.json( + { error: "同じ開催日で時間帯が重複しています" }, + { status: 400 } + ); + } + // Raw SQL で保存 await prisma.$executeRawUnsafe( `INSERT INTO reserve_setting @@ -163,7 +177,7 @@ export async function POST(req: NextRequest) { parseInt(data.priceAdult, 10), parseInt(data.priceChild, 10), data.limitMethod, - data.limitUnit, + data.limitUnit, // NOTE: フロントのキー名に合わせて現状維持(DB列は limit_type) parseInt(data.limitValue, 10) );