refactor(backend): 내려받기 머리글을 download-headers 한 곳으로 모은다

휴대폰 앱 받기(다음 커밋)가 대용량 첨부 받기와 같은 머리글을 쓰게 하려고,
file-sharing 의 writeFileHeaders 가 만들던 머리글을 shared/utils/download-headers.ts 로 옮겼다.
머리글 이름 · 값 · 차례는 그대로다(새 단위 시험과 대용량 받기의 단위 · e2e 가 지킨다).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 23:29:02 +09:00
parent 178a53fd6f
commit 1f8555340e
3 changed files with 85 additions and 16 deletions
@@ -21,6 +21,7 @@ import {
import type { StoredFileView } from '@/modules/file-storage/file-storage.types';
import { currentRequestId, traceTag } from '@/shared/logger/request-context';
import { buildAttachmentDisposition } from '@/shared/utils/content-disposition';
import { downloadHeaders } from '@/shared/utils/download-headers';
import { decideRange } from '@/shared/utils/http-range';
import { kstEndOfDayAfter } from '@/shared/utils/kst-date';
import { maskEmail } from '@/shared/utils/mask';
@@ -67,10 +68,8 @@ import {
/** 보낸 링크 목록의 커서 범위 (shared/utils/offset-cursor.ts) */
const SHARE_LINKS_SCOPE = 'share-links';
/** 파일 응답의 머리글 (계약 4절 "파일 응답") — 형식은 서버가 정한다(늘 내려받기) */
/** 파일 응답의 형식 (계약 4절 "파일 응답") — 서버가 정한다(늘 내려받기). 나머지 머리글은 shared/utils/download-headers.ts */
const FILE_CONTENT_TYPE = 'application/octet-stream';
const FILE_CSP = "default-src 'none'; sandbox";
const FILE_CACHE_CONTROL = 'private, no-store';
/** 내려받기 — 누가 받나 */
export interface DownloadOptions {
@@ -634,25 +633,24 @@ export class FileSharingService implements BeforeApplicationShutdown {
});
}
/** 파일 응답의 머리글 (계약 4절 "파일 응답") — 형식 · 이름 · 검증자 · 캐시 · 버퍼링 */
/**
* 파일 응답의 머리글 (계약 4절 "파일 응답") — 형식 · 이름 · 검증자 · 캐시 · 버퍼링.
* 휴대폰 앱 받기와 같은 꼴이다(shared/utils/download-headers.ts — 형식만 다르다)
*/
private writeFileHeaders(
res: Response,
name: string,
etag: string,
lastModified: Date,
): void {
res.setHeader('Content-Type', FILE_CONTENT_TYPE);
res.setHeader('Content-Disposition', buildAttachmentDisposition(name));
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Security-Policy', FILE_CSP);
res.setHeader('Cache-Control', FILE_CACHE_CONTROL);
res.setHeader('Referrer-Policy', 'no-referrer');
res.setHeader('X-Robots-Tag', 'noindex');
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified.toUTCString());
// 앞단 nginx · 외부 프록시가 이 응답을 임시 파일에 받아 두지 않게 (느린 받는 사람마다 1GB 까지)
res.setHeader('X-Accel-Buffering', 'no');
for (const [header, value] of downloadHeaders({
contentType: FILE_CONTENT_TYPE,
contentDisposition: buildAttachmentDisposition(name),
etag,
lastModified,
})) {
res.setHeader(header, value);
}
}
private writeLength(