94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { promises as fs } from "fs";
|
|
import path from "path";
|
|
import { PrismaClient } from "@prisma/client";
|
|
|
|
const UPLOAD_DIR = path.join(process.cwd(), "uploads");
|
|
|
|
let prisma: PrismaClient | null = null;
|
|
if (process.env.DATABASE_URL && process.env.DATABASE_URL.trim() !== "") {
|
|
try {
|
|
prisma = new PrismaClient();
|
|
} catch (error) {
|
|
// DB 가 준비되지 않은 환경에서는 mimeType 조회 없이 기본값만 사용한다.
|
|
console.error("PrismaClient 초기화 실패 - 기본 MIME 타입으로만 응답합니다.", error);
|
|
prisma = null;
|
|
}
|
|
}
|
|
|
|
export async function GET(request: Request) {
|
|
// Next.js 15 이후 params 가 Promise 로 전달되는 경고를 피하기 위해,
|
|
// 동적 세그먼트는 request.url 의 path 에서 직접 파싱한다.
|
|
let id: string | undefined;
|
|
|
|
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 });
|
|
}
|
|
}
|