UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

387 lines (338 loc) 10.5 kB
import { CDNService, getCDNService } from "./cdn-service"; import { queueImageProcessing, getProcessingStatus } from "./image-worker"; export interface MediaUploadOptions { enableCDN?: boolean; enableProcessing?: boolean; processingSizes?: string[]; processingFormats?: string[]; customMetadata?: Record<string, unknown>; tags?: string[]; description?: string; } export interface MediaUploadResult { id: string; fileName: string; originalName: string; mimeType: string; size: number; url: string; cdnUrl?: string; thumbnailUrl?: string; uploadedAt: Date; processingJobId?: string; metadata?: Record<string, unknown>; tags?: string[]; description?: string; } export interface MediaFile { id: string; fileName: string; originalName: string; mimeType: string; size: number; url: string; cdnUrl?: string; thumbnailUrl?: string; createdAt: string; updatedAt: string; downloadCount: number; viewCount: number; tags: string[]; description?: string; dimensions?: { width: number; height: number; }; metadata?: Record<string, unknown>; processingStatus?: "pending" | "processing" | "completed" | "failed"; processingJobId?: string; } export class MediaService { private cdnService: CDNService; private uploadPath: string; constructor() { this.cdnService = getCDNService(); this.uploadPath = process.env.MEDIA_UPLOAD_PATH || "./uploads"; } async uploadFile( file: Buffer, originalName: string, mimeType: string, options: MediaUploadOptions = {} ): Promise<MediaUploadResult> { const fileId = this.generateFileId(); const fileName = this.generateFileName(originalName, fileId); const fileKey = this.generateFileKey(fileName); try { // 로컬 저장소에 저장 const localUrl = await this.saveToLocal(file, fileName); // CDN 업로드 (활성화된 경우) let cdnUrl: string | undefined; if (options.enableCDN !== false) { try { const cdnResult = await this.cdnService.upload( file, fileKey, mimeType ); cdnUrl = cdnResult.cdnUrl || cdnResult.url; } catch (error) { console.warn("CDN upload failed, using local storage:", error); } } // 이미지 처리 큐에 추가 (이미지 파일인 경우) let processingJobId: string | undefined; if (options.enableProcessing !== false && mimeType.startsWith("image/")) { try { const jobId = await queueImageProcessing(fileId, localUrl, fileName, { sizes: (options.processingSizes as | ("thumbnail" | "small" | "medium" | "large" | "xlarge")[] | undefined) || ["thumbnail", "small", "medium", "large"], generateWebP: options.processingFormats?.includes("webp") || true, generateAVIF: options.processingFormats?.includes("avif") || false, quality: 85, preserveMetadata: true, optimize: true, }); processingJobId = jobId; } catch (error) { console.warn("Image processing queue failed:", error); } } // 메타데이터 저장 const mediaFile: MediaUploadResult = { id: fileId, fileName, originalName, mimeType, size: file.length, url: localUrl, cdnUrl, uploadedAt: new Date(), processingJobId, metadata: options.customMetadata, tags: options.tags, description: options.description, }; await this.saveMetadata(mediaFile); return mediaFile; } catch (error) { console.error("File upload failed:", error); throw new Error(`파일 업로드 실패: ${error}`); } } async getFile(fileId: string): Promise<MediaFile | null> { try { const metadata = await this.getMetadata(fileId); if (!metadata) return null; // 처리 상태 업데이트 (처리 중인 경우) if ( metadata.processingJobId && metadata.processingStatus !== "completed" ) { const processingStatus = await getProcessingStatus( metadata.processingJobId ); if (processingStatus) { metadata.processingStatus = processingStatus.status; await this.updateMetadata(fileId, { processingStatus: processingStatus.status, }); } } return metadata; } catch (error) { console.error("Failed to get file:", error); return null; } } async getFiles( options: { page?: number; limit?: number; mimeType?: string; tags?: string[]; searchTerm?: string; sortBy?: "name" | "date" | "size"; sortOrder?: "asc" | "desc"; } = {} ): Promise<{ files: MediaFile[]; total: number; page: number; limit: number; totalPages: number; }> { // 실제 구현에서는 데이터베이스에서 조회 // 여기서는 Mock 데이터 반환 const mockFiles: MediaFile[] = []; return { files: mockFiles, total: 0, page: options.page || 1, limit: options.limit || 20, totalPages: 0, }; } async updateFile( fileId: string, updates: Partial<Pick<MediaFile, "tags" | "description" | "metadata">> ): Promise<MediaFile | null> { try { const existingFile = await this.getFile(fileId); if (!existingFile) return null; const updatedFile = { ...existingFile, ...updates, updatedAt: new Date().toISOString(), }; await this.updateMetadata(fileId, updatedFile); // CDN에서 캐시 무효화 (메타데이터가 URL에 영향을 주는 경우) if (existingFile.cdnUrl) { try { await this.cdnService.invalidate([ this.generateFileKey(existingFile.fileName), ]); } catch (error) { console.warn("CDN cache invalidation failed:", error); } } return updatedFile; } catch (error) { console.error("Failed to update file:", error); return null; } } async deleteFile(fileId: string): Promise<boolean> { try { const file = await this.getFile(fileId); if (!file) return false; // 로컬 파일 삭제 await this.deleteFromLocal(file.fileName); // CDN에서 삭제 if (file.cdnUrl) { try { await this.cdnService.delete(this.generateFileKey(file.fileName)); } catch (error) { console.warn("CDN delete failed:", error); } } // 메타데이터 삭제 await this.deleteMetadata(fileId); return true; } catch (error) { console.error("Failed to delete file:", error); return false; } } async getFileUrl( fileId: string, options?: { width?: number; height?: number; format?: string; quality?: number; preferCDN?: boolean; } ): Promise<string | null> { const file = await this.getFile(fileId); if (!file) return null; // CDN URL 사용 (가능한 경우) if (options?.preferCDN !== false && file.cdnUrl) { return this.cdnService.getUrl( this.generateFileKey(file.fileName), options ); } return file.url; } async incrementDownloadCount(fileId: string): Promise<void> { const file = await this.getFile(fileId); if (file) { await this.updateMetadata(fileId, { downloadCount: file.downloadCount + 1, }); } } async incrementViewCount(fileId: string): Promise<void> { const file = await this.getFile(fileId); if (file) { await this.updateMetadata(fileId, { viewCount: file.viewCount + 1, }); } } // CDN 통계 및 상태 async getCDNStats() { return this.cdnService.checkHealth(); } async refreshCDNCache(fileIds: string[]): Promise<boolean> { try { const keys = await Promise.all( fileIds.map(async (fileId) => { const file = await this.getFile(fileId); return file ? this.generateFileKey(file.fileName) : null; }) ); const validKeys = keys.filter((key): key is string => key !== null); if (validKeys.length > 0) { await this.cdnService.invalidate(validKeys); } return true; } catch (error) { console.error("Failed to refresh CDN cache:", error); return false; } } // 헬퍼 메서드들 private generateFileId(): string { return Date.now().toString() + Math.random().toString(36).substr(2, 9); } private generateFileName(originalName: string, fileId: string): string { const ext = originalName.split(".").pop(); const baseName = originalName.replace(/\.[^/.]+$/, ""); return `${fileId}_${baseName}.${ext}`; } private generateFileKey(fileName: string): string { return `media/${new Date().getFullYear()}/${ new Date().getMonth() + 1 }/${fileName}`; } private async saveToLocal(file: Buffer, fileName: string): Promise<string> { // 실제 구현에서는 파일 시스템에 저장 // 여기서는 Mock URL 반환 return `/uploads/${fileName}`; } private async saveMetadata(metadata: MediaUploadResult): Promise<void> { // 실제 구현에서는 데이터베이스에 저장 console.log("Saving metadata:", metadata); } private async getMetadata(fileId: string): Promise<MediaFile | null> { // 실제 구현에서는 데이터베이스에서 조회 console.log("Getting metadata for:", fileId); return null; } private async updateMetadata( fileId: string, updates: Partial<MediaFile> ): Promise<void> { // 실제 구현에서는 데이터베이스 업데이트 console.log("Updating metadata:", fileId, updates); } private async deleteMetadata(fileId: string): Promise<void> { // 실제 구현에서는 데이터베이스에서 삭제 console.log("Deleting metadata:", fileId); } private async deleteFromLocal(fileName: string): Promise<void> { // 실제 구현에서는 파일 시스템에서 삭제 console.log("Deleting local file:", fileName); } } // 싱글톤 인스턴스 let mediaService: MediaService | null = null; export function getMediaService(): MediaService { if (!mediaService) { mediaService = new MediaService(); } return mediaService; }