File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import { getSessionAccessToken, verifySession } from '@/lib/auth/dal';
import { backendFetchUpload } from '@/lib/http/backend-fetch';
import { findFileModule } from '@/lib/domain/file-module';
/**
* 큰 파일 업로드 중계 — 브라우저 → 이 라우트 → 백엔드로 본문을 그대로 흘려보낸다.
*
* Server Action을 쓰지 않는 이유는 그 경로의 multipart가 10MB 부근에서 잘리기 때문이다
* (`MAX_ATTACHMENT_BYTES` 주석). 브라우저가 백엔드를 직접 부를 수는 없다 — 토큰이 httpOnly
* 세션 안에만 있어서 여기서 붙여 준다.
*
* 크기를 여기서 막지 않는다(사용자 확정). 본문을 버퍼에 담지 않고 그대로 흘려보내므로 이
* 라우트에는 상한이 필요 없고, 실제 한도는 앞단 프록시와 백엔드가 정한다 — 넘으면 413이
* 사유와 함께 화면까지 올라간다.
*/
const UPLOAD_PATH = '/api/v1/common/file/upload';
function fail(status: number, message: string): Response {
return Response.json({ message }, { status });
}
export async function POST(request: Request) {
await verifySession();
const moduleId = new URL(request.url).searchParams.get('module') ?? '';
const storage = findFileModule(moduleId);
if (!storage) {
return fail(400, '알 수 없는 저장소입니다.');
}
const contentType = request.headers.get('content-type') ?? '';
if (!contentType.startsWith('multipart/form-data')) {
return fail(400, '요청 형식이 올바르지 않습니다.');
}
if (!request.body) {
return fail(400, '파일이 없습니다.');
}
const accessToken = await getSessionAccessToken();
const result = await backendFetchUpload<unknown>(
`${UPLOAD_PATH}/${storage.id}`,
{
body: request.body,
contentType,
accessToken: accessToken ?? undefined,
}
);
if (!result.ok) {
// 프록시가 자른 413은 크기 문제라 사용자에게 그대로 보인다. 나머지는 중계 실패다.
return fail(result.code === 413 ? 413 : 502, result.message);
}
if (typeof result.data !== 'string' || result.data === '') {
return fail(502, '업로드 응답에 파일 식별자가 없습니다.');
}
return Response.json({ id: result.data });
}