/** * 파일명에서 확장자를 추출한다. (브라우저·서버 공용, node:path 미사용) * @param {string} [filename] - 파일명 * @returns {string} 소문자 확장자(점 포함, 예: `.mp4`) */ const getFileExtension = (filename = '') => { const normalized = String(filename).trim() const lastDot = normalized.lastIndexOf('.') if (lastDot <= 0 || lastDot === normalized.length - 1) { return '' } return normalized.slice(lastDot).toLowerCase() } /** @type {Record} 업로드 종류 */ export const UPLOAD_KIND = { image: 'image', video: 'video', audio: 'audio', document: 'document' } const VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov']) const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.m4a']) const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']) export const DEFAULT_UPLOAD_MAX_FILE_COUNT = 20 export const DEFAULT_UPLOAD_MAX_TOTAL_SIZE = 314572800 export const MIN_UPLOAD_MAX_FILE_COUNT = 1 export const MAX_UPLOAD_MAX_FILE_COUNT = 50 export const MIN_UPLOAD_MAX_TOTAL_SIZE = 1048576 export const MAX_UPLOAD_MAX_TOTAL_SIZE = 1073741824 export const UPLOAD_FILE_COUNT_OPTIONS = [5, 10, 20, 30, 50] export const UPLOAD_TOTAL_SIZE_OPTIONS = [52428800, 104857600, 209715200, 314572800, 524288000, 1073741824] /** @type {Record} 업로드 종류 한글 라벨 */ const UPLOAD_KIND_LABELS = { [UPLOAD_KIND.image]: '이미지', [UPLOAD_KIND.video]: '비디오', [UPLOAD_KIND.audio]: '오디오', [UPLOAD_KIND.document]: '파일' } /** * 기본 업로드 크기 한도(바이트)를 만든다. * @param {Object} [overrides] - 종류별 한도 덮어쓰기 * @returns {{ image: number, video: number, audio: number, document: number }} 종류별 한도 */ export const buildDefaultUploadSizeLimits = (overrides = {}) => ({ image: 10485760, video: 209715200, audio: 52428800, document: 52428800, ...overrides }) /** * 업로드 최대 파일 수를 정규화한다. * @param {unknown} value - 원본 값 * @returns {number} 최대 파일 수 */ export const normalizeUploadMaxFileCount = (value) => { const parsed = Number(value) if (!Number.isFinite(parsed)) { return DEFAULT_UPLOAD_MAX_FILE_COUNT } const rounded = Math.round(parsed) if (rounded < MIN_UPLOAD_MAX_FILE_COUNT) { return MIN_UPLOAD_MAX_FILE_COUNT } if (rounded > MAX_UPLOAD_MAX_FILE_COUNT) { return MAX_UPLOAD_MAX_FILE_COUNT } return rounded } /** * 업로드 최대 총량을 정규화한다. * @param {unknown} value - 원본 값 * @returns {number} 최대 총량 바이트 */ export const normalizeUploadMaxTotalSize = (value) => { const parsed = Number(value) if (!Number.isFinite(parsed)) { return DEFAULT_UPLOAD_MAX_TOTAL_SIZE } const rounded = Math.round(parsed) if (rounded < MIN_UPLOAD_MAX_TOTAL_SIZE) { return MIN_UPLOAD_MAX_TOTAL_SIZE } if (rounded > MAX_UPLOAD_MAX_TOTAL_SIZE) { return MAX_UPLOAD_MAX_TOTAL_SIZE } return rounded } /** * 업로드 파일 크기를 반환한다. * @param {Object} file - 브라우저 File 또는 서버 multipart 파일 파트 * @returns {number} 파일 크기 */ export const getUploadFileSize = (file) => { const size = file?.size ?? file?.data?.length ?? 0 const parsed = Number(size) return Number.isFinite(parsed) && parsed > 0 ? parsed : 0 } /** * 업로드 파일 전체 크기를 반환한다. * @param {Iterable|ArrayLike} files - 업로드 파일 목록 * @returns {number} 전체 크기 */ export const getUploadFilesTotalSize = (files) => Array.from(files || []) .reduce((total, file) => total + getUploadFileSize(file), 0) /** * MIME·파일명으로 업로드 종류를 판별한다. * @param {string} [mimeType] - MIME 타입 * @param {string} [filename] - 파일명 * @returns {'image'|'video'|'audio'|'document'} 업로드 종류 */ export const getUploadKind = (mimeType = '', filename = '') => { const extension = getFileExtension(filename) if (mimeType.startsWith('video/') || VIDEO_EXTENSIONS.has(extension)) { return UPLOAD_KIND.video } if (mimeType.startsWith('audio/') || AUDIO_EXTENSIONS.has(extension)) { return UPLOAD_KIND.audio } if (mimeType.startsWith('image/') || IMAGE_EXTENSIONS.has(extension)) { return UPLOAD_KIND.image } return UPLOAD_KIND.document } /** * 업로드 종류별 최대 바이트를 반환한다. * @param {'image'|'video'|'audio'|'document'} kind - 업로드 종류 * @param {{ image: number, video: number, audio: number, document: number }} limits - 종류별 한도 * @returns {number} 최대 바이트 */ export const getMaxUploadBytesForKind = (kind, limits) => limits[kind] ?? limits.image /** * 바이트를 사람이 읽기 쉬운 용량 문자열로 변환한다. * @param {number} bytes - 바이트 * @returns {string} 용량 문자열 */ export const formatUploadSizeLimit = (bytes) => { if (!Number.isFinite(bytes) || bytes <= 0) { return '0B' } if (bytes >= 1073741824) { const gigabytes = bytes / 1073741824 return `${Number.isInteger(gigabytes) ? gigabytes : gigabytes.toFixed(1)}GB` } if (bytes >= 1048576) { return `${Math.round(bytes / 1048576)}MB` } if (bytes >= 1024) { return `${Math.round(bytes / 1024)}KB` } return `${bytes}B` } /** * 업로드 종류 한글 라벨을 반환한다. * @param {'image'|'video'|'audio'|'document'} kind - 업로드 종류 * @returns {string} 한글 라벨 */ export const getUploadKindLabel = (kind) => UPLOAD_KIND_LABELS[kind] || UPLOAD_KIND_LABELS[UPLOAD_KIND.image]