이미지 파일 업로드 기능
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
|
||||
const UPLOAD_DIR = path.join(process.cwd(), "uploads");
|
||||
|
||||
let prisma: PrismaClient | null = null;
|
||||
try {
|
||||
prisma = new PrismaClient();
|
||||
} catch (error) {
|
||||
// DB 가 준비되지 않은 환경에서는 mimeType 조회 없이 기본값만 사용한다.
|
||||
console.error("PrismaClient 초기화 실패 - 기본 MIME 타입으로만 응답합니다.", error);
|
||||
prisma = null;
|
||||
}
|
||||
|
||||
interface Params {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: Request, ctx: Params) {
|
||||
// Next.js 가 params 를 항상 보장하지 않는 상황을 대비해, URL 경로에서 id 를 보완적으로 파싱한다.
|
||||
let id: string | undefined = ctx?.params?.id;
|
||||
if (!id) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const segments = url.pathname.split("/").filter(Boolean);
|
||||
// [..., "api", "image", ":id"] 형태를 기대한다.
|
||||
const last = segments[segments.length - 1];
|
||||
if (last && last !== "image") {
|
||||
id = last;
|
||||
}
|
||||
} catch {
|
||||
// URL 파싱 실패 시에는 그대로 둔다.
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ message: "id 파라미터가 필요합니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
// Referer 기반으로 간단한 핫링크 방지: 동일 호스트에서 온 요청만 허용한다.
|
||||
try {
|
||||
const requestUrl = new URL(request.url);
|
||||
const referer = request.headers.get("referer");
|
||||
|
||||
if (!referer) {
|
||||
return NextResponse.json({ message: "이미지 접근이 허용되지 않습니다." }, { status: 403 });
|
||||
}
|
||||
|
||||
const refererUrl = new URL(referer);
|
||||
if (refererUrl.host !== requestUrl.host) {
|
||||
return NextResponse.json({ message: "이미지 접근이 허용되지 않습니다." }, { status: 403 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Referer 검사 중 오류", error);
|
||||
return NextResponse.json({ message: "이미지 접근이 허용되지 않습니다." }, { status: 403 });
|
||||
}
|
||||
|
||||
const filePath = path.join(UPLOAD_DIR, id);
|
||||
|
||||
try {
|
||||
const bytes = await fs.readFile(filePath);
|
||||
|
||||
// 기본 MIME 타입은 png 로 두되, 가능하면 Asset 메타데이터에서 실제 타입을 읽어온다.
|
||||
let mimeType = "image/png";
|
||||
if (prisma) {
|
||||
try {
|
||||
const asset = await prisma.asset.findUnique({ where: { id } });
|
||||
const meta: any = asset?.meta ?? null;
|
||||
const candidate = typeof meta?.mimeType === "string" ? meta.mimeType.trim() : "";
|
||||
if (candidate) {
|
||||
mimeType = candidate;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("이미지 메타데이터 조회 실패 - 기본 MIME 타입으로 응답합니다.", error);
|
||||
}
|
||||
}
|
||||
|
||||
return new NextResponse(bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": mimeType,
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error && typeof error === "object" && (error as any).code === "ENOENT") {
|
||||
return NextResponse.json({ message: "이미지를 찾을 수 없습니다." }, { status: 404 });
|
||||
}
|
||||
|
||||
console.error("이미지 파일 읽기 중 오류", error);
|
||||
return NextResponse.json({ message: "이미지 로드 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user