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 { backendFetchStream } from '@/lib/http/backend-fetch';
/**
* 게시판 첨부파일 다운로드 — 백엔드 파일을 브라우저로 중계한다.
*
* 왜 라우트 핸들러인가: 브라우저는 백엔드를 직접 호출하지 않는다(설계서 §7 BFF 전제). 백엔드 파일
* API는 토큰을 요구하는데 그 토큰은 httpOnly 세션 안에만 있어 브라우저가 꺼낼 수 없다. 학생 회원
* 엑셀 다운로드와 같은 구조다.
*
* 본문은 파싱하지 않고 업스트림 스트림을 그대로 흘려보낸다(서버 메모리에 파일 전체를 올리지
* 않는다). 파일명·MIME도 백엔드 헤더를 그대로 전달한다.
*/
const FILE_DOWNLOAD_PATH = '/api/v1/common/file/download';
const DOWNLOAD_FAILED_MESSAGE =
'첨부파일을 내려받지 못했습니다. 잠시 후 다시 시도해 주세요.';
const DOWNLOAD_TIMEOUT_MS = 60_000;
export async function GET(request: Request) {
// 라우트 핸들러는 UI를 거치지 않고 직접 호출될 수 있으므로 여기서 직접 인증을 확인한다.
await verifySession();
const url = new URL(request.url);
const attachmentId = url.searchParams.get('atchFileId');
if (!attachmentId) {
return new Response('첨부파일 식별자가 없습니다.', {
status: 400,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
// 백엔드는 파일 일련번호(fileSn)로 개별 파일을 지목한다. 게시판은 파일 하나만 붙이므로 1이
// 기본이며, 여러 개를 붙이게 되면 호출부가 번호를 넘긴다.
const fileSn = url.searchParams.get('fileSn') ?? '1';
const accessToken = await getSessionAccessToken();
const result = await backendFetchStream(FILE_DOWNLOAD_PATH, {
query: { atchFileId: attachmentId, fileSn },
accessToken: accessToken ?? undefined,
timeoutMs: DOWNLOAD_TIMEOUT_MS,
});
if (!result.ok) {
// 실패 사유는 backendFetchStream이 서버 콘솔에 남긴다. 화면에는 일반화된 문구만 내보낸다.
return new Response(DOWNLOAD_FAILED_MESSAGE, {
status: 502,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
const upstream = result.data;
return new Response(upstream.body, {
status: 200,
headers: {
'Content-Type':
upstream.headers.get('content-type') ?? 'application/octet-stream',
'Content-Disposition':
upstream.headers.get('content-disposition') ?? 'attachment',
},
});
}