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 { 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',
},
});
}