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( `${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 }); }