File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
/**
* 파일 저장소 모듈 — 업로드 시 `POST /api/v1/common/file/upload/{moduleId}`의 경로 조각이자
* 백엔드 파일 저장 설정(`FileStrgStngVo.strgStngId`)의 키다.
*
* 값은 백엔드 저장소 설정 표에서 받은 실제 목록이다(사용자 제공, 2026-08-20).
*
* 이슈: 설정에 없는 값을 보내도 업로드가 실패하지 않는다. 백엔드가 설정을 못 찾으면 기본
* 저장소로 폴백해 조용히 다른 곳에 저장한다(`EgovFileMngUtil.parseFileInf`). 그래서 여기 없는
* 문자열을 손으로 적지 않는다.
*/
export type FileModule = {
id: string;
/** 저장소 설명 — 어떤 파일이 어디로 가는지 이 파일만 보고 알 수 있게 함께 둔다. */
description: string;
/** 백엔드가 허용하는 확장자. 화면의 `accept`와 사전 검증이 같은 값을 본다. */
extensions: readonly string[];
};
/**
* 업로드 전체에 걸린 확장자 화이트리스트 — 백엔드 `Globals.fileUpload.Extensions`.
* `EgovMultipartResolver`가 저장소 설정보다 **먼저** 이 목록으로 거른다.
*
* 이슈: 저장소 설정이 허용해도 여기 없으면 못 올린다. `MODULE_VOD_CONTENT`는 mov·avi·mkv·
* webm·wmv를 허용하지만 실제로 통과하는 것은 mp4뿐이고, svg도 마찬가지로 막힌다.
* 백엔드에 이 목록 확장을 요청하면 그때 여기만 늘리면 된다.
*/
export const GLOBAL_UPLOAD_EXTENSIONS: readonly string[] = [
'gif', 'jpg', 'jpeg', 'png',
'xls', 'xlsx', 'ppt', 'pptx', 'doc', 'docx', 'hwp', 'pdf',
'mp4', 'mp3', 'txt', 'html', 'htm',
];
const IMAGE = ['jpg', 'png', 'gif'] as const;
export const FILE_MODULE = {
/** 콘텐츠(금융쇼츠·4컷금융만화) 대표 이미지. */
contentThumbnail: {
id: 'MODULE_CONTENT_THUMB',
description: '콘텐츠 섬네일 이미지 저장소',
extensions: IMAGE,
},
/** 4컷금융만화의 컷 이미지. */
toonContent: {
id: 'MODULE_TOON_CONTENT',
description: '4컷 만화 파일 저장소',
extensions: IMAGE,
},
/** 금융쇼츠의 영상. */
vodContent: {
id: 'MODULE_VOD_CONTENT',
description: '동영상 콘텐츠 저장소',
// 이슈: 백엔드 전역 화이트리스트(Globals.fileUpload.Extensions)에 영상은 mp4뿐이다.
// mov·avi 등은 목록에 추가되면 여기도 되돌린다.
extensions: ['mp4'],
},
/** 꾸미기 아이템 썸네일. */
userItem: {
id: 'MODULE_USER_ITEM',
description: '사용자 아이템 저장소',
extensions: ['jpg', 'png', 'gif', 'svg'],
},
menuIcon: {
id: 'MODULE_MENU_ICON',
description: '메뉴 아이콘 저장소',
extensions: ['jpg', 'png', 'gif', 'svg'],
},
pocket: {
id: 'MODULE_POCKET',
description: '용돈기입장 사진 저장소',
extensions: ['jpg', 'jpeg', 'png', 'gif'],
},
studentImage: {
id: 'MODULE_STUDENT_IMAGE',
description: '학생 원본 이미지',
extensions: IMAGE,
},
userProfile: {
id: 'MODULE_USER_PROFILE',
description: '회원 프로필 저장소',
extensions: IMAGE,
},
/** 게시판(공지사항·FAQ·1:1문의) 첨부. */
board: {
id: 'MODULE_BBS',
description: '게시글 스토리지',
// TODO: 이 저장소만의 허용 확장자를 받지 못했다. 전역 화이트리스트를 그대로 두어 화면이
// 먼저 막지 않게 한다 — 저장소 목록이 더 좁으면 백엔드가 사유를 돌려준다.
extensions: GLOBAL_UPLOAD_EXTENSIONS,
},
} as const satisfies Record<string, FileModule>;
/** 실제로 통과하는 확장자 — 저장소 설정과 전역 화이트리스트의 교집합이다. */
export function allowedExtensions(module: FileModule): string[] {
return module.extensions.filter((extension) =>
GLOBAL_UPLOAD_EXTENSIONS.includes(extension)
);
}
/** 확장자 목록 → `<input accept>` 값. 화면이 백엔드 허용 목록을 직접 베끼지 않게 한다. */
export function fileModuleAccept(module: FileModule): string {
return allowedExtensions(module)
.map((extension) => `.${extension}`)
.join(',');
}
/** 파일명의 확장자가 허용 목록에 있는지. 백엔드가 사유 없이 실패하기 전에 먼저 잡는다. */
export function isAllowedFileExtension(
module: FileModule,
fileName: string
): boolean {
const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
return allowedExtensions(module).includes(extension);
}
export function findFileModule(id: string): FileModule | undefined {
return Object.values(FILE_MODULE).find((module) => module.id === id);
}
/**
* Server Action을 지나는 첨부파일의 상한.
*
* 이슈: 미들웨어(`proxy.ts`)를 지나는 요청은 본문이 10MiB에서 잘린다(측정치는 그 파일 주석
* 참조). Server Action은 페이지 URL로 POST되어 반드시 미들웨어를 지나므로 이 한도를 피할 수
* 없다. 더 큰 파일은 미들웨어에서 제외한 스트리밍 라우트(`/contents/upload`)로 보낸다.
*/
export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
/** 스트리밍 업로드 상한 — 백엔드 `multipartResolver`의 `maxUploadSize`(100,000,000)와 같은 값. */
export const MAX_STREAM_UPLOAD_BYTES = 100_000_000;