diff --git a/app/components/my-notebooks/ImportCardsForm.tsx b/app/components/my-notebooks/ImportCardsForm.tsx index 7bb1e95..2bd97e8 100644 --- a/app/components/my-notebooks/ImportCardsForm.tsx +++ b/app/components/my-notebooks/ImportCardsForm.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { useFetcher } from "react-router"; +import { MAX_EXCEL_UPLOAD_SIZE } from "@/lib/shared/excel-constraints"; import type { FormState } from "@/lib/shared/form-state"; const initialState: FormState = {}; @@ -174,6 +175,9 @@ export default function ImportCardsForm({ notebookId }: { notebookId: string }) required className="text-sm text-zinc-600 file:mr-3 file:rounded-full file:border-0 file:bg-coral-100 file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-coral-700 hover:file:bg-coral-200 hover:file:text-coral-800 dark:text-zinc-400 dark:file:bg-coral-900/30 dark:file:text-coral-300 dark:hover:file:bg-coral-900/50 dark:hover:file:text-coral-200" /> +
+ ファイルサイズは最大 {MAX_EXCEL_UPLOAD_SIZE / (1024 * 1024)} MB です。 +
1行目を見出し行として読み取ります。既存の列と同じ名前の列はそこに追加され、無い列名は新しい列として追加されます。
diff --git a/app/lib/server/my-notebooks/actions.server.test.ts b/app/lib/server/my-notebooks/actions.server.test.ts index d9f6f74..dab9b52 100644 --- a/app/lib/server/my-notebooks/actions.server.test.ts +++ b/app/lib/server/my-notebooks/actions.server.test.ts @@ -20,11 +20,13 @@ import { MAX_STAR_COUNT, NOTEBOOK_TITLE_MAX_LENGTH, } from "@/lib/shared/notebook-schema"; +import { EXCEL_UPLOAD_SIZE_ERROR, MAX_EXCEL_UPLOAD_SIZE } from "@/lib/shared/excel-constraints"; import { addNotebookColumn, createBlankNotebook, createCard as createCardAction, + importCardsFromExcel, deleteCard, deleteNotebook, moveCard, @@ -585,6 +587,17 @@ describe("★(toggleStar / setStarCount)", () => { }); describe("入力の検証", () => { + it("10 MB を超える Excel ファイルは解析せず拒否する", async () => { + loginAs(owner); + const formData = new FormData(); + formData.set("file", new File([new Uint8Array(MAX_EXCEL_UPLOAD_SIZE + 1)], "large.xlsx")); + + expect(await importCardsFromExcel(notebook.id, formData)).toEqual({ + error: EXCEL_UPLOAD_SIZE_ERROR, + }); + expect(await prisma.card.count({ where: { notebookId: notebook.id } })).toBe(0); + }); + it("長すぎるタイトルの単語帳は作成しない", async () => { loginAs(owner); const before = await prisma.notebook.count({ where: { userId: owner.id } }); diff --git a/app/lib/server/my-notebooks/actions.server.ts b/app/lib/server/my-notebooks/actions.server.ts index 6b7634e..61775a1 100644 --- a/app/lib/server/my-notebooks/actions.server.ts +++ b/app/lib/server/my-notebooks/actions.server.ts @@ -4,6 +4,7 @@ import { prisma } from "@/lib/server/prisma.server"; import { requireUser } from "@/lib/server/session.server"; import { requireNotebookRole } from "@/lib/server/notebook-access.server"; import { ExcelParseError, parseExcelWorkbook } from "@/lib/shared/excel"; +import { EXCEL_UPLOAD_SIZE_ERROR, MAX_EXCEL_UPLOAD_SIZE } from "@/lib/shared/excel-constraints"; import { normalizeCardData, type CardData } from "@/lib/shared/card-data"; import { DEFAULT_HEAD_COLUMN, @@ -96,6 +97,9 @@ export async function importCardsFromExcel( if (!(file instanceof File) || file.size === 0) { return { error: "Excelファイル(.xlsx)を選択してください。" }; } + if (file.size > MAX_EXCEL_UPLOAD_SIZE) { + return { error: EXCEL_UPLOAD_SIZE_ERROR }; + } const notebook = await prisma.notebook.findUniqueOrThrow({ where: { id: notebookId }, diff --git a/app/lib/shared/excel-constraints.ts b/app/lib/shared/excel-constraints.ts new file mode 100644 index 0000000..f222f6a --- /dev/null +++ b/app/lib/shared/excel-constraints.ts @@ -0,0 +1,2 @@ +export const MAX_EXCEL_UPLOAD_SIZE = 10 * 1024 * 1024; +export const EXCEL_UPLOAD_SIZE_ERROR = "Excelファイルは10 MB以下にしてください。"; diff --git a/app/lib/shared/excel.test.ts b/app/lib/shared/excel.test.ts index b766826..2973b00 100644 --- a/app/lib/shared/excel.test.ts +++ b/app/lib/shared/excel.test.ts @@ -2,6 +2,7 @@ import ExcelJS from "exceljs"; import { describe, expect, it } from "vite-plus/test"; import { createTemplateWorkbookBuffer, ExcelParseError, parseExcelWorkbook } from "./excel"; +import { MAX_EXCEL_UPLOAD_SIZE } from "./excel-constraints"; async function workbookBuffer(rows: (string | number | null)[][]): Promise