@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
265 lines (224 loc) • 6.86 kB
text/typescript
import { MediaConfig } from "../../types/media";
// =============================================================================
// Default Media Configuration
// =============================================================================
export const DEFAULT_MEDIA_CONFIG: MediaConfig = {
storage: {
provider: "local",
path: "/uploads",
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedTypes: [
// Images
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"image/webp",
"image/svg+xml",
"image/bmp",
"image/tiff",
// Documents
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"text/plain",
"text/csv",
// Archives
"application/zip",
"application/x-rar-compressed",
"application/x-7z-compressed",
// Audio
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/mp4",
// Video
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/x-msvideo",
"video/webm",
],
},
processing: {
enabled: true,
thumbnailSizes: [150, 300, 600, 1200],
webpConversion: true,
qualitySettings: {
jpeg: 85,
webp: 80,
png: 90,
},
},
cdn: {
enabled: false,
cacheHeaders: {
"Cache-Control": "public, max-age=31536000, immutable",
},
},
security: {
virusScanning: false, // Enable if ClamAV or similar is available
checksumValidation: true,
signedUrls: false,
maxQuotaPerSite: 1024 * 1024 * 1024, // 1GB per site
maxQuotaPerUser: 100 * 1024 * 1024, // 100MB per user
},
features: {
folders: true,
collections: true,
tags: true,
analytics: true,
watermarking: false,
},
};
// =============================================================================
// Configuration Helpers
// =============================================================================
export function getMediaConfig(): MediaConfig {
// In a real app, this would merge environment variables or database settings
return DEFAULT_MEDIA_CONFIG;
}
export function isFileTypeAllowed(mimeType: string): boolean {
const config = getMediaConfig();
return config.storage.allowedTypes.includes(mimeType);
}
export function getMaxFileSize(): number {
const config = getMediaConfig();
return config.storage.maxFileSize;
}
export function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
export function getFileTypeCategory(
mimeType: string
): "image" | "document" | "audio" | "video" | "archive" | "other" {
if (mimeType.startsWith("image/")) return "image";
if (mimeType.startsWith("audio/")) return "audio";
if (mimeType.startsWith("video/")) return "video";
if (
mimeType.includes("pdf") ||
mimeType.includes("document") ||
mimeType.includes("sheet") ||
mimeType.includes("presentation") ||
mimeType.includes("text/")
)
return "document";
if (
mimeType.includes("zip") ||
mimeType.includes("rar") ||
mimeType.includes("7z")
)
return "archive";
return "other";
}
export function generateThumbnailSizes(): number[] {
const config = getMediaConfig();
return config.processing.thumbnailSizes;
}
export function shouldProcessFile(mimeType: string): boolean {
const config = getMediaConfig();
return config.processing.enabled && mimeType.startsWith("image/");
}
// =============================================================================
// File Validation
// =============================================================================
export interface FileValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
}
export function validateFile(file: File): FileValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// Check file type
if (!isFileTypeAllowed(file.type)) {
errors.push(`파일 타입 '${file.type}'은(는) 허용되지 않습니다.`);
}
// Check file size
const maxSize = getMaxFileSize();
if (file.size > maxSize) {
errors.push(
`파일 크기가 너무 큽니다. 최대 ${formatFileSize(
maxSize
)}까지 업로드 가능합니다.`
);
}
// Check file name
if (file.name.length > 255) {
errors.push("파일명이 너무 깁니다. 255자 이하로 줄여주세요.");
}
// Check for dangerous file names
const dangerousPatterns = [
/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i,
/[<>:"|?*]/,
/^\.+$/,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(file.name)) {
errors.push("파일명에 허용되지 않는 문자가 포함되어 있습니다.");
break;
}
}
// Warnings for large files
if (file.size > 5 * 1024 * 1024) {
// 5MB
warnings.push("파일이 큽니다. 업로드에 시간이 걸릴 수 있습니다.");
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
export function validateBatchUpload(files: File[]): FileValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (files.length === 0) {
errors.push("업로드할 파일을 선택해주세요.");
return { isValid: false, errors, warnings };
}
if (files.length > 50) {
errors.push("한 번에 최대 50개까지 업로드할 수 있습니다.");
}
let totalSize = 0;
const duplicateNames = new Set<string>();
const fileNames = new Set<string>();
for (const file of files) {
totalSize += file.size;
// Check for duplicate names
if (fileNames.has(file.name)) {
duplicateNames.add(file.name);
}
fileNames.add(file.name);
// Validate individual file
const validation = validateFile(file);
errors.push(...validation.errors);
warnings.push(...validation.warnings);
}
// Check total size
if (totalSize > 100 * 1024 * 1024) {
// 100MB
warnings.push(
"전체 파일 크기가 큽니다. 업로드에 시간이 오래 걸릴 수 있습니다."
);
}
// Check for duplicates
if (duplicateNames.size > 0) {
warnings.push(
`중복된 파일명이 있습니다: ${Array.from(duplicateNames).join(", ")}`
);
}
return {
isValid: errors.length === 0,
errors: [...new Set(errors)], // Remove duplicates
warnings: [...new Set(warnings)],
};
}