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