From 9e75989c5d19e0c2ce155e7c788cb76e8eafcf37 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Mon, 1 Dec 2025 17:24:19 +0900 Subject: [PATCH 1/2] =?UTF-8?q?playwright=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 8 ++++++++ package.json | 1 + playwright.config.ts | 8 ++++++-- uploads/268f2133-6f72-44a5-ab84-729fdd8887fe | 1 + uploads/59618a73-4686-44da-86a3-44cbd8ff5fc8 | 1 + uploads/6a17dd96-2e73-439e-9524-789c522dad07 | 1 + uploads/b2c93be7-3970-411d-96a1-e481f51d9c18 | 1 + uploads/d761f0a1-b02a-4fd3-b510-6b50528e2050 | 1 + 8 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 uploads/268f2133-6f72-44a5-ab84-729fdd8887fe create mode 100644 uploads/59618a73-4686-44da-86a3-44cbd8ff5fc8 create mode 100644 uploads/6a17dd96-2e73-439e-9524-789c522dad07 create mode 100644 uploads/b2c93be7-3970-411d-96a1-e481f51d9c18 create mode 100644 uploads/d761f0a1-b02a-4fd3-b510-6b50528e2050 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 47a81c4..370b74d 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -29,6 +29,14 @@ jobs: run: | npx prisma generate + - name: Build Next app + env: + # 빌드 시에도 Prisma Client 가 필요하므로 더미 DATABASE_URL 을 사용한다. + DATABASE_URL: postgresql://example:example@localhost:5432/example + PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: "1" + run: | + npm run build + - name: Run unit tests if: github.event_name == 'push' env: diff --git a/package.json b/package.json index 5b4aa7b..31b433b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "next dev", "build": "next build", "start": "next start", + "start:e2e": "next start -p 3000", "lint": "next lint", "test": "vitest run", "e2e": "playwright test" diff --git a/playwright.config.ts b/playwright.config.ts index f9235c7..64ac299 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,7 @@ import { defineConfig, devices } from "@playwright/test"; +const isCI = !!process.env.CI; + export default defineConfig({ testDir: "./tests/e2e", timeout: 30_000, @@ -17,9 +19,11 @@ export default defineConfig({ }, ], webServer: { - command: "npm run dev", + // CI 에서는 프로덕션 빌드 서버(next start)를 대상으로 E2E 를 수행해 + // dev 모드의 느린 HMR/StrictMode 2회 렌더로 인한 flakiness 를 줄인다. + command: isCI ? "npm run start:e2e" : "npm run dev", url: "http://localhost:3000", - reuseExistingServer: !process.env.CI, + reuseExistingServer: !isCI, timeout: 120_000, }, }); diff --git a/uploads/268f2133-6f72-44a5-ab84-729fdd8887fe b/uploads/268f2133-6f72-44a5-ab84-729fdd8887fe new file mode 100644 index 0000000..b9954d9 --- /dev/null +++ b/uploads/268f2133-6f72-44a5-ab84-729fdd8887fe @@ -0,0 +1 @@ +PNG TEST FIXTURE diff --git a/uploads/59618a73-4686-44da-86a3-44cbd8ff5fc8 b/uploads/59618a73-4686-44da-86a3-44cbd8ff5fc8 new file mode 100644 index 0000000..b9954d9 --- /dev/null +++ b/uploads/59618a73-4686-44da-86a3-44cbd8ff5fc8 @@ -0,0 +1 @@ +PNG TEST FIXTURE diff --git a/uploads/6a17dd96-2e73-439e-9524-789c522dad07 b/uploads/6a17dd96-2e73-439e-9524-789c522dad07 new file mode 100644 index 0000000..b9954d9 --- /dev/null +++ b/uploads/6a17dd96-2e73-439e-9524-789c522dad07 @@ -0,0 +1 @@ +PNG TEST FIXTURE diff --git a/uploads/b2c93be7-3970-411d-96a1-e481f51d9c18 b/uploads/b2c93be7-3970-411d-96a1-e481f51d9c18 new file mode 100644 index 0000000..b9954d9 --- /dev/null +++ b/uploads/b2c93be7-3970-411d-96a1-e481f51d9c18 @@ -0,0 +1 @@ +PNG TEST FIXTURE diff --git a/uploads/d761f0a1-b02a-4fd3-b510-6b50528e2050 b/uploads/d761f0a1-b02a-4fd3-b510-6b50528e2050 new file mode 100644 index 0000000..b9954d9 --- /dev/null +++ b/uploads/d761f0a1-b02a-4fd3-b510-6b50528e2050 @@ -0,0 +1 @@ +PNG TEST FIXTURE -- 2.52.0 From c3de7df252426cdfe0ea268650d6f0c0e351c224 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Mon, 1 Dec 2025 18:00:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next-env.d.ts | 2 +- src/app/api/video/[id]/route.ts | 14 +++- src/app/editor/page.tsx | 10 ++- tests/api/videoServe.spec.ts | 82 ++++++++++++++++++++ uploads/386e38d1-736e-4dd2-90db-1e80990f9ddc | 1 + uploads/fb58aea5-1c53-4b99-b683-a2d6bee52f37 | 1 + uploads/test-image-1764578878778 | 1 + uploads/test-image-1764579185471 | 1 + uploads/test-section-bg-1764578878787 | 1 + uploads/test-section-bg-1764579185620 | 1 + uploads/test-section-bg-video-1764578878873 | 1 + uploads/test-section-bg-video-1764579185705 | 1 + uploads/test-video-1764578878841 | 1 + uploads/test-video-1764579185695 | 1 + uploads/test-video-poster-1764578878782 | 1 + uploads/test-video-poster-1764579185505 | 1 + 16 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 tests/api/videoServe.spec.ts create mode 100644 uploads/386e38d1-736e-4dd2-90db-1e80990f9ddc create mode 100644 uploads/fb58aea5-1c53-4b99-b683-a2d6bee52f37 create mode 100644 uploads/test-image-1764578878778 create mode 100644 uploads/test-image-1764579185471 create mode 100644 uploads/test-section-bg-1764578878787 create mode 100644 uploads/test-section-bg-1764579185620 create mode 100644 uploads/test-section-bg-video-1764578878873 create mode 100644 uploads/test-section-bg-video-1764579185705 create mode 100644 uploads/test-video-1764578878841 create mode 100644 uploads/test-video-1764579185695 create mode 100644 uploads/test-video-poster-1764578878782 create mode 100644 uploads/test-video-poster-1764579185505 diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/api/video/[id]/route.ts b/src/app/api/video/[id]/route.ts index 5c94d1f..ff77e7c 100644 --- a/src/app/api/video/[id]/route.ts +++ b/src/app/api/video/[id]/route.ts @@ -15,14 +15,22 @@ try { } interface Params { - params: { + params: Promise<{ id: string; - }; + }>; } export async function GET(request: Request, ctx: Params) { // Next.js 가 params 를 항상 보장하지 않는 상황을 대비해, URL 경로에서 id 를 보완적으로 파싱한다. - let id: string | undefined = ctx?.params?.id; + let id: string | undefined; + + try { + const resolved = await ctx.params; + id = resolved?.id; + } catch { + id = undefined; + } + if (!id) { try { const url = new URL(request.url); diff --git a/src/app/editor/page.tsx b/src/app/editor/page.tsx index 5ddf261..c578c5f 100644 --- a/src/app/editor/page.tsx +++ b/src/app/editor/page.tsx @@ -1,7 +1,7 @@ "use client"; import type { CSSProperties, ReactNode } from "react"; -import { Fragment, useEffect, useState } from "react"; +import { Fragment, Suspense, useEffect, useState } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { FilePlus2, Trash2, Eye, Pencil, ListChecks, Undo2, Redo2 } from "lucide-react"; @@ -71,6 +71,14 @@ import { PropertiesSidebar } from "./panels/PropertiesSidebar"; import { EditorCanvas } from "./EditorCanvas"; export default function EditorPage() { + return ( + + + + ); +} + +function EditorPageInner() { const router = useRouter(); const searchParams = useSearchParams(); const blocks = useEditorStore((state) => state.blocks); diff --git a/tests/api/videoServe.spec.ts b/tests/api/videoServe.spec.ts new file mode 100644 index 0000000..bee7525 --- /dev/null +++ b/tests/api/videoServe.spec.ts @@ -0,0 +1,82 @@ +import "dotenv/config"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { promises as fs } from "fs"; +import path from "path"; + +const BASE_URL = "http://localhost"; + +// /api/video/:id 서브 라우트 TDD +// - 존재하는 업로드 파일 id 로 GET /api/video/:id 를 호출하면 200 과 비디오 바이트를 반환해야 한다. +// - 존재하지 않는 id 로 호출하면 404 를 반환해야 한다. + +// Prisma 엔진이 실제로 로드되지 않도록 PrismaClient 를 단순 목으로 대체한다. +vi.mock("@prisma/client", () => { + class PrismaClient { + asset = { + // GET /api/video/:id 에서는 MIME 타입 조회에만 사용되므로, 테스트에서는 null 을 반환한다. + findUnique: async () => null, + }; + } + + return { PrismaClient }; +}); + +describe("/api/video/:id 서브 라우트", () => { + const uploadsDir = path.join(process.cwd(), "uploads"); + let testId: string; + let filePath: string; + + beforeEach(async () => { + await fs.mkdir(uploadsDir, { recursive: true }); + testId = `test-video-${Date.now()}`; + filePath = path.join(uploadsDir, testId); + await fs.writeFile(filePath, Buffer.from("dummy-video-bytes")); + }); + + afterEach(async () => { + try { + await fs.unlink(filePath); + } catch { + // 이미 삭제된 경우는 무시한다. + } + }); + + it("업로드된 비디오 파일이 존재하면 200 과 비디오 바이트를 반환해야 한다", async () => { + const { GET: handleGet } = await import("@/app/api/video/[id]/route"); + + const requestUrl = `${BASE_URL}/api/video/${testId}`; + const res = await handleGet( + new Request(requestUrl, { + headers: { + // Referer 기반 핫링크 방지 로직이 있으므로, 동일 호스트의 referer 를 포함한다. + referer: `${BASE_URL}/editor`, + }, + }), + // Next 16 타입 정의에서는 context.params 가 Promise 형태이므로, Promise 로 감싼다. + { params: Promise.resolve({ id: testId }) } as any, + ); + + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toBe("video/mp4"); + + const arrayBuffer = await res.arrayBuffer(); + const buf = Buffer.from(arrayBuffer); + expect(buf.length).toBeGreaterThan(0); + }); + + it("존재하지 않는 비디오 id 로 요청하면 404 를 반환해야 한다", async () => { + const { GET: handleGet } = await import("@/app/api/video/[id]/route"); + + const missingId = `missing-${Date.now()}`; + const res = await handleGet( + new Request(`${BASE_URL}/api/video/${missingId}`, { + headers: { + referer: `${BASE_URL}/editor`, + }, + }), + { params: Promise.resolve({ id: missingId }) } as any, + ); + + expect(res.status).toBe(404); + }); +}); diff --git a/uploads/386e38d1-736e-4dd2-90db-1e80990f9ddc b/uploads/386e38d1-736e-4dd2-90db-1e80990f9ddc new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/386e38d1-736e-4dd2-90db-1e80990f9ddc @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/fb58aea5-1c53-4b99-b683-a2d6bee52f37 b/uploads/fb58aea5-1c53-4b99-b683-a2d6bee52f37 new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/fb58aea5-1c53-4b99-b683-a2d6bee52f37 @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/test-image-1764578878778 b/uploads/test-image-1764578878778 new file mode 100644 index 0000000..0cc5db2 --- /dev/null +++ b/uploads/test-image-1764578878778 @@ -0,0 +1 @@ +dummy-image \ No newline at end of file diff --git a/uploads/test-image-1764579185471 b/uploads/test-image-1764579185471 new file mode 100644 index 0000000..0cc5db2 --- /dev/null +++ b/uploads/test-image-1764579185471 @@ -0,0 +1 @@ +dummy-image \ No newline at end of file diff --git a/uploads/test-section-bg-1764578878787 b/uploads/test-section-bg-1764578878787 new file mode 100644 index 0000000..0cc5db2 --- /dev/null +++ b/uploads/test-section-bg-1764578878787 @@ -0,0 +1 @@ +dummy-image \ No newline at end of file diff --git a/uploads/test-section-bg-1764579185620 b/uploads/test-section-bg-1764579185620 new file mode 100644 index 0000000..0cc5db2 --- /dev/null +++ b/uploads/test-section-bg-1764579185620 @@ -0,0 +1 @@ +dummy-image \ No newline at end of file diff --git a/uploads/test-section-bg-video-1764578878873 b/uploads/test-section-bg-video-1764578878873 new file mode 100644 index 0000000..badc796 --- /dev/null +++ b/uploads/test-section-bg-video-1764578878873 @@ -0,0 +1 @@ +dummy-section-video \ No newline at end of file diff --git a/uploads/test-section-bg-video-1764579185705 b/uploads/test-section-bg-video-1764579185705 new file mode 100644 index 0000000..badc796 --- /dev/null +++ b/uploads/test-section-bg-video-1764579185705 @@ -0,0 +1 @@ +dummy-section-video \ No newline at end of file diff --git a/uploads/test-video-1764578878841 b/uploads/test-video-1764578878841 new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/test-video-1764578878841 @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/test-video-1764579185695 b/uploads/test-video-1764579185695 new file mode 100644 index 0000000..87ccacf --- /dev/null +++ b/uploads/test-video-1764579185695 @@ -0,0 +1 @@ +dummy-video \ No newline at end of file diff --git a/uploads/test-video-poster-1764578878782 b/uploads/test-video-poster-1764578878782 new file mode 100644 index 0000000..7285c92 --- /dev/null +++ b/uploads/test-video-poster-1764578878782 @@ -0,0 +1 @@ +dummy-poster \ No newline at end of file diff --git a/uploads/test-video-poster-1764579185505 b/uploads/test-video-poster-1764579185505 new file mode 100644 index 0000000..7285c92 --- /dev/null +++ b/uploads/test-video-poster-1764579185505 @@ -0,0 +1 @@ +dummy-poster \ No newline at end of file -- 2.52.0