同日で時刻が被らない開催を登録できるように変更

This commit is contained in:
system_master 2026-01-24 19:18:54 +09:00
parent 2cd12da2f6
commit 60bb6935c9
3 changed files with 186 additions and 174 deletions

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3004", "dev": "next dev -p 3004",
"build": "next build", "build": "dotenv -e .env.production -- next build",
"start": "dotenv -e .env.production -- next start", "start": "dotenv -e .env.production -- next start",
"lint": "eslint" "lint": "eslint"
}, },

View File

@ -1,13 +1,14 @@
// src/app/api/reserve-setting/[id]/route.ts // src/app/api/reserve-setting/[id]/route.ts
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { validateReserveSetting } from "@/lib/validators/reserveSettingValidator"; import { validateReserveSetting } from "@/lib/validators/reserveSettingValidator";
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { type RouteContext = { params: Promise<{ id: string }> };
export async function GET(req: NextRequest, context: RouteContext) {
try { try {
const { id } = await params; const { id } = await context.params;
const settingId = parseInt(id, 10); const settingId = parseInt(id, 10);
const setting = await prisma.reserveSetting.findUnique({ const setting = await prisma.reserveSetting.findUnique({
@ -28,7 +29,6 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
return NextResponse.json({ error: "開催が見つかりません" }, { status: 404 }); return NextResponse.json({ error: "開催が見つかりません" }, { status: 404 });
} }
// 予約数集計
let allCount = 0; let allCount = 0;
const blockCounts: Record<string, number> = {}; const blockCounts: Record<string, number> = {};
@ -75,54 +75,59 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
} }
} }
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function PUT(req: NextRequest, context: RouteContext) {
try { try {
const { id } = await params; const { id } = await context.params;
const settingId = Number(id); const settingId = parseInt(id, 10);
const data = await req.json(); const data = await req.json();
// 必須チェック
if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) { if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) {
return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 }); return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 });
} }
const now = dayjs();
const dinerDate = dayjs(data.dinerDate); const dinerDate = dayjs(data.dinerDate);
if (dinerDate.isBefore(dayjs())) { if (dinerDate.isBefore(now)) {
return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 }); return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 });
} }
// 開催日重複チェック(自分以外) // 同日開催OK時間重複はNG
const checkDate = dinerDate.format("YYYY-MM-DD"); 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 }[]>( const overlaps = await prisma.$queryRawUnsafe<{ dummy: number }[]>(
`SELECT 1 as dummy FROM reserve_setting WHERE diner_date = ? AND delete_flag = 0 AND autonum <> ? LIMIT 1`, `
checkDate, SELECT 1 as dummy
data.id FROM reserve_setting
WHERE diner_date = ?
AND delete_flag = 0
AND autonum <> ?
AND start_time < ?
AND end_time > ?
LIMIT 1
`,
dinerDateStr,
settingId,
newEndTime,
newStartTime
); );
console.log([checkDate, data.id, exists]);
if (exists.length > 0) { if (overlaps.length > 0) {
return NextResponse.json( return NextResponse.json(
{ error: "同じ開催日が既に登録されています" }, { error: "同じ開催日で時間が重複する開催が既に登録されています" },
{ status: 400 } { status: 400 }
); );
} }
// 共通バリデーション
const validationError = validateReserveSetting(data); const validationError = validateReserveSetting(data);
if (validationError) { if (validationError) {
return NextResponse.json({ error: validationError }, { status: 400 }); return NextResponse.json({ error: validationError }, { 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 reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss");
const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss"); const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss");
// Raw SQL UPDATE
await prisma.$executeRawUnsafe( await prisma.$executeRawUnsafe(
`UPDATE reserve_setting `UPDATE reserve_setting
SET diner_date = ?, start_time = ?, end_time = ?, time_range = ?, SET diner_date = ?, start_time = ?, end_time = ?, time_range = ?,
@ -131,8 +136,8 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
reserve_limit = ?, note = ?, lastupdate = NOW() reserve_limit = ?, note = ?, lastupdate = NOW()
WHERE autonum = ?`, WHERE autonum = ?`,
dinerDateStr, dinerDateStr,
startTimeStr, newStartTime,
endTimeStr, newEndTime,
Number(data.timeRange), Number(data.timeRange),
reserveStartStr, reserveStartStr,
reserveLimitStr, reserveLimitStr,
@ -152,29 +157,22 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
} }
} }
interface DeleteExists { export async function DELETE(req: NextRequest, context: RouteContext) {
autonum: number;
}
// DELETE: 論理削除Raw SQL版
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try { try {
const { id } = await params; const { id } = await context.params;
const settingId = parseInt(id, 10); const settingId = parseInt(id, 10);
// 存在確認
const rows = (await prisma.$queryRawUnsafe( const rows = (await prisma.$queryRawUnsafe(
`SELECT autonum FROM reserve_setting WHERE autonum = ? AND delete_flag = 0 LIMIT 1`, `SELECT autonum FROM reserve_setting WHERE autonum = ? AND delete_flag = 0 LIMIT 1`,
settingId settingId
)) as DeleteExists[]; )) as { autonum: number }[];
if (rows.length === 0) { if (rows.length === 0) {
return NextResponse.json({ error: "対象データが存在しません" }, { status: 404 }); return NextResponse.json({ error: "対象データが存在しません" }, { status: 404 });
} }
// 論理削除
await prisma.$executeRawUnsafe( await prisma.$executeRawUnsafe(
`UPDATE reserve_setting `UPDATE reserve_setting SET delete_flag = 1, lastupdate = NOW() WHERE autonum = ?`,
SET delete_flag = 1, lastupdate = NOW()
WHERE autonum = ?`,
settingId settingId
); );

View File

@ -72,8 +72,6 @@ export async function GET() {
diner_range: `${dayjs(row.diner_date).format("M/DD")}\n${startHM}~${endHM}`, diner_range: `${dayjs(row.diner_date).format("M/DD")}\n${startHM}~${endHM}`,
end_hm: endHM, end_hm: endHM,
// ここは SQL 側で '%Y-%m-%d %H:%i:%s' の文字列にして返しているので
// その文字列を dayjs のフォーマットで ISO 風に成形
reserve_start: row.reserve_start_str reserve_start: row.reserve_start_str
? dayjs(row.reserve_start_str, "YYYY-MM-DD HH:mm:ss").format("YYYY-MM-DDTHH:mm") ? 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) { export async function POST(req: NextRequest) {
try { try {
const data = await req.json(); const data = await req.json();
console.log("受信:", data.timeRange);
// 必須チェック // 必須チェック
if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) { if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) {
return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 }); return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 });
} }
// 過去日禁止 // 過去日禁止
const now = dayjs(); const now = dayjs();
const dinerDate = dayjs(data.dinerDate); const dinerDate = dayjs(data.dinerDate);
if (dinerDate.isBefore(now)) { if (dinerDate.isBefore(now)) {
return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 }); 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); const validationError = validateReserveSetting(data);
if (validationError) { if (validationError) {
return NextResponse.json({ error: validationError }, { status: 400 }); return NextResponse.json({ error: validationError }, { status: 400 });
} }
// JST文字列に変換 // JST文字列に変換※時間重複チェックでも使う
const dinerDateStr = dayjs(data.dinerDate).format("YYYY-MM-DD"); const dinerDateStr = dayjs(data.dinerDate).format("YYYY-MM-DD");
const startTimeStr = dayjs(data.dinerDate).format("HH:mm:ss"); const startTimeStr = dayjs(data.dinerDate).format("HH:mm:ss");
const endTimeStr = dayjs(`${data.dinerDate.split("T")[0]}T${data.endTime}`).format( 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 reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss");
const reserveLimitStr = dayjs(data.reserveLimitDate).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 で保存 // Raw SQL で保存
await prisma.$executeRawUnsafe( await prisma.$executeRawUnsafe(
`INSERT INTO reserve_setting `INSERT INTO reserve_setting
@ -163,7 +177,7 @@ export async function POST(req: NextRequest) {
parseInt(data.priceAdult, 10), parseInt(data.priceAdult, 10),
parseInt(data.priceChild, 10), parseInt(data.priceChild, 10),
data.limitMethod, data.limitMethod,
data.limitUnit, data.limitUnit, // NOTE: フロントのキー名に合わせて現状維持DB列は limit_type
parseInt(data.limitValue, 10) parseInt(data.limitValue, 10)
); );