Merge pull request 'CI: feature/editor-account-switch' (#22) from feature/editor-account-switch into main
CI / test (push) Successful in 6m0s
CI / pr_and_merge (push) Has been skipped

This commit was merged in pull request #22.
This commit is contained in:
2025-12-01 09:06:14 +00:00
24 changed files with 135 additions and 7 deletions
+8
View File
@@ -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:
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.
+1
View File
@@ -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"
+6 -2
View File
@@ -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,
},
});
+11 -3
View File
@@ -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);
+9 -1
View File
@@ -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 (
<Suspense fallback={null}>
<EditorPageInner />
</Suspense>
);
}
function EditorPageInner() {
const router = useRouter();
const searchParams = useSearchParams();
const blocks = useEditorStore((state) => state.blocks);
+82
View File
@@ -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);
});
});
@@ -0,0 +1 @@
PNG TEST FIXTURE
@@ -0,0 +1 @@
dummy-video
@@ -0,0 +1 @@
PNG TEST FIXTURE
@@ -0,0 +1 @@
PNG TEST FIXTURE
@@ -0,0 +1 @@
PNG TEST FIXTURE
@@ -0,0 +1 @@
PNG TEST FIXTURE
@@ -0,0 +1 @@
dummy-video
+1
View File
@@ -0,0 +1 @@
dummy-image
+1
View File
@@ -0,0 +1 @@
dummy-image
+1
View File
@@ -0,0 +1 @@
dummy-image
+1
View File
@@ -0,0 +1 @@
dummy-image
@@ -0,0 +1 @@
dummy-section-video
@@ -0,0 +1 @@
dummy-section-video
+1
View File
@@ -0,0 +1 @@
dummy-video
+1
View File
@@ -0,0 +1 @@
dummy-video
+1
View File
@@ -0,0 +1 @@
dummy-poster
+1
View File
@@ -0,0 +1 @@
dummy-poster