File name
Commit message
Commit date
File name
Commit message
Commit date
09-04
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 });
}