UNPKG

@restnfeel/agentc-starter-kit

Version:

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

609 lines (545 loc) 15 kB
export interface BatchJob { id: string; type: BatchJobType; status: BatchJobStatus; userId: string; fileIds: string[]; operation: BatchOperation; progress: BatchProgress; result?: BatchResult; createdAt: Date; startedAt?: Date; completedAt?: Date; error?: string; metadata?: Record<string, unknown>; } export enum BatchJobType { BULK_UPLOAD = "bulk_upload", BULK_DELETE = "bulk_delete", BULK_EDIT = "bulk_edit", BULK_MOVE = "bulk_move", BULK_PROCESS = "bulk_process", BULK_TAG = "bulk_tag", BULK_WATERMARK = "bulk_watermark", BULK_DOWNLOAD = "bulk_download", } export enum BatchJobStatus { PENDING = "pending", QUEUED = "queued", PROCESSING = "processing", COMPLETED = "completed", FAILED = "failed", CANCELLED = "cancelled", PAUSED = "paused", } export interface BatchOperation { type: BatchJobType; params: Record<string, unknown>; options?: { priority?: "low" | "normal" | "high"; retryCount?: number; timeout?: number; concurrency?: number; }; } export interface BatchProgress { total: number; completed: number; failed: number; currentFile?: string; estimatedTimeRemaining?: number; throughput?: number; // files per second } export interface BatchResult { successful: string[]; failed: Array<{ fileId: string; error: string; }>; totalProcessed: number; totalTime: number; averageTime: number; summary?: Record<string, unknown>; } export interface BulkEditParams { metadata?: { fileName?: string; description?: string; altText?: string; }; tags?: { add?: string[]; remove?: string[]; replace?: string[]; }; collection?: { add?: string[]; remove?: string[]; }; folder?: { moveToId?: string; }; [key: string]: unknown; } export interface BulkProcessParams { imageProcessing?: { resize?: { width?: number; height?: number }; format?: "jpeg" | "png" | "webp" | "avif"; quality?: number; watermark?: { text?: string; position?: | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "center"; opacity?: number; }; }; [key: string]: unknown; } export class BatchProcessor { private jobs: Map<string, BatchJob> = new Map(); private workerPool: Map<string, NodeJS.Timeout> = new Map(); private maxConcurrentJobs: number = 3; private retryAttempts: number = 3; private retryDelay: number = 5000; // 5초 constructor(maxConcurrentJobs?: number) { this.maxConcurrentJobs = maxConcurrentJobs || 3; } /** * 배치 작업 생성 및 큐에 추가 */ async createBatchJob( userId: string, type: BatchJobType, fileIds: string[], operation: BatchOperation, metadata?: Record<string, unknown> ): Promise<string> { const jobId = `batch_${Date.now()}_${Math.random() .toString(36) .substr(2, 9)}`; const job: BatchJob = { id: jobId, type, status: BatchJobStatus.PENDING, userId, fileIds, operation, progress: { total: fileIds.length, completed: 0, failed: 0, }, createdAt: new Date(), metadata, }; this.jobs.set(jobId, job); // 작업을 큐에 추가 await this.queueJob(jobId); return jobId; } /** * 대량 업로드 작업 생성 */ async createBulkUpload( userId: string, files: File[], options?: { folderId?: string; tags?: string[]; enableProcessing?: boolean; enableCDN?: boolean; } ): Promise<string> { // 파일을 임시 저장하고 ID 생성 const fileIds = files.map((_, index) => `temp_${Date.now()}_${index}`); return this.createBatchJob( userId, BatchJobType.BULK_UPLOAD, fileIds, { type: BatchJobType.BULK_UPLOAD, params: { files: files.map((file) => ({ name: file.name, size: file.size, type: file.type, })), options, }, }, { originalFileCount: files.length } ); } /** * 대량 편집 작업 생성 */ async createBulkEdit( userId: string, fileIds: string[], editParams: BulkEditParams ): Promise<string> { return this.createBatchJob(userId, BatchJobType.BULK_EDIT, fileIds, { type: BatchJobType.BULK_EDIT, params: editParams, }); } /** * 대량 삭제 작업 생성 */ async createBulkDelete( userId: string, fileIds: string[], options?: { moveToTrash?: boolean; permanentDelete?: boolean; } ): Promise<string> { return this.createBatchJob(userId, BatchJobType.BULK_DELETE, fileIds, { type: BatchJobType.BULK_DELETE, params: { options }, }); } /** * 대량 처리 작업 생성 (이미지 최적화, 워터마킹 등) */ async createBulkProcess( userId: string, fileIds: string[], processParams: BulkProcessParams ): Promise<string> { return this.createBatchJob(userId, BatchJobType.BULK_PROCESS, fileIds, { type: BatchJobType.BULK_PROCESS, params: processParams, }); } /** * 대량 태그 작업 생성 */ async createBulkTag( userId: string, fileIds: string[], tagOperation: { action: "add" | "remove" | "replace"; tags: string[]; } ): Promise<string> { return this.createBatchJob(userId, BatchJobType.BULK_TAG, fileIds, { type: BatchJobType.BULK_TAG, params: { tagOperation }, }); } /** * 대량 워터마크 작업 생성 */ async createBulkWatermark( userId: string, fileIds: string[], watermarkOptions: { text?: string; image?: string; position?: | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "center"; opacity?: number; } ): Promise<string> { return this.createBatchJob(userId, BatchJobType.BULK_WATERMARK, fileIds, { type: BatchJobType.BULK_WATERMARK, params: { watermarkOptions }, }); } /** * 대량 다운로드 작업 생성 */ async createBulkDownload( userId: string, fileIds: string[], options?: { format?: "zip" | "tar"; compression?: boolean; } ): Promise<string> { return this.createBatchJob(userId, BatchJobType.BULK_DOWNLOAD, fileIds, { type: BatchJobType.BULK_DOWNLOAD, params: { options }, }); } /** * 작업을 큐에 추가 */ private async queueJob(jobId: string): Promise<void> { const job = this.jobs.get(jobId); if (!job) return; job.status = BatchJobStatus.QUEUED; // 현재 실행 중인 작업 수 확인 const runningJobs = Array.from(this.jobs.values()).filter( (j) => j.status === BatchJobStatus.PROCESSING ).length; if (runningJobs < this.maxConcurrentJobs) { await this.startJob(jobId); } } /** * 작업 실행 시작 */ private async startJob(jobId: string): Promise<void> { const job = this.jobs.get(jobId); if (!job || job.status !== BatchJobStatus.QUEUED) return; job.status = BatchJobStatus.PROCESSING; job.startedAt = new Date(); try { await this.executeJob(job); job.status = BatchJobStatus.COMPLETED; job.completedAt = new Date(); } catch (error) { job.status = BatchJobStatus.FAILED; job.error = error instanceof Error ? error.message : "Unknown error"; job.completedAt = new Date(); } // 다음 대기 중인 작업 시작 await this.startNextQueuedJob(); } /** * 다음 대기 중인 작업 시작 */ private async startNextQueuedJob(): Promise<void> { const queuedJob = Array.from(this.jobs.values()).find( (job) => job.status === BatchJobStatus.QUEUED ); if (queuedJob) { await this.startJob(queuedJob.id); } } /** * 작업 실행 */ private async executeJob(job: BatchJob): Promise<void> { const startTime = Date.now(); const successful: string[] = []; const failed: Array<{ fileId: string; error: string }> = []; for (let i = 0; i < job.fileIds.length; i++) { const fileId = job.fileIds[i]; try { job.progress.currentFile = fileId; await this.processFile(job, fileId); successful.push(fileId); job.progress.completed++; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; failed.push({ fileId, error: errorMessage }); job.progress.failed++; } // 처리량 계산 const elapsed = (Date.now() - startTime) / 1000; job.progress.throughput = (job.progress.completed + job.progress.failed) / elapsed; // 예상 완료 시간 계산 const remaining = job.progress.total - job.progress.completed - job.progress.failed; job.progress.estimatedTimeRemaining = remaining / (job.progress.throughput || 1); } const totalTime = Date.now() - startTime; job.result = { successful, failed, totalProcessed: successful.length + failed.length, totalTime, averageTime: totalTime / job.fileIds.length, }; } /** * 개별 파일 처리 */ private async processFile(job: BatchJob, fileId: string): Promise<void> { switch (job.type) { case BatchJobType.BULK_UPLOAD: await this.processUpload(job, fileId); break; case BatchJobType.BULK_EDIT: await this.processEdit(job, fileId); break; case BatchJobType.BULK_DELETE: await this.processDelete(job, fileId); break; case BatchJobType.BULK_PROCESS: await this.processImage(job, fileId); break; default: throw new Error(`Unsupported job type: ${job.type}`); } } /** * 업로드 처리 */ private async processUpload(job: BatchJob, fileId: string): Promise<void> { // 실제 구현에서는 파일 업로드 로직 수행 await new Promise((resolve) => setTimeout(resolve, 1000)); // Mock delay console.log(`Uploading file ${fileId} for job ${job.id}`); } /** * 편집 처리 */ private async processEdit(job: BatchJob, fileId: string): Promise<void> { // 실제 구현에서는 메타데이터 편집 로직 수행 await new Promise((resolve) => setTimeout(resolve, 500)); // Mock delay console.log(`Editing file ${fileId} for job ${job.id}`); } /** * 삭제 처리 */ private async processDelete(job: BatchJob, fileId: string): Promise<void> { // 실제 구현에서는 파일 삭제 로직 수행 await new Promise((resolve) => setTimeout(resolve, 300)); // Mock delay console.log(`Deleting file ${fileId} for job ${job.id}`); } /** * 이미지 처리 */ private async processImage(job: BatchJob, fileId: string): Promise<void> { // 실제 구현에서는 이미지 처리 로직 수행 await new Promise((resolve) => setTimeout(resolve, 2000)); // Mock delay console.log(`Processing image ${fileId} for job ${job.id}`); } /** * 작업 상태 조회 */ async getJobStatus(jobId: string): Promise<BatchJob | null> { return this.jobs.get(jobId) || null; } /** * 사용자별 작업 목록 조회 */ async getUserJobs(userId: string, limit: number = 50): Promise<BatchJob[]> { return Array.from(this.jobs.values()) .filter((job) => job.userId === userId) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) .slice(0, limit); } /** * 활성 작업 목록 조회 */ async getActiveJobs(): Promise<BatchJob[]> { return Array.from(this.jobs.values()).filter( (job) => job.status === BatchJobStatus.PROCESSING || job.status === BatchJobStatus.QUEUED ); } /** * 작업 취소 */ async cancelJob(jobId: string): Promise<boolean> { const job = this.jobs.get(jobId); if (!job) return false; if (job.status === BatchJobStatus.QUEUED) { job.status = BatchJobStatus.CANCELLED; return true; } if (job.status === BatchJobStatus.PROCESSING) { // 실행 중인 작업은 다음 파일 처리 시 중단 job.status = BatchJobStatus.CANCELLED; return true; } return false; } /** * 작업 일시정지 */ async pauseJob(jobId: string): Promise<boolean> { const job = this.jobs.get(jobId); if (!job || job.status !== BatchJobStatus.PROCESSING) return false; job.status = BatchJobStatus.PAUSED; return true; } /** * 작업 재개 */ async resumeJob(jobId: string): Promise<boolean> { const job = this.jobs.get(jobId); if (!job || job.status !== BatchJobStatus.PAUSED) return false; job.status = BatchJobStatus.QUEUED; await this.queueJob(jobId); return true; } /** * 통계 조회 */ async getStats(): Promise<{ totalJobs: number; activeJobs: number; completedJobs: number; failedJobs: number; averageProcessingTime: number; queueLength: number; }> { const jobs = Array.from(this.jobs.values()); const activeJobs = jobs.filter( (j) => j.status === BatchJobStatus.PROCESSING || j.status === BatchJobStatus.QUEUED ).length; const completedJobs = jobs.filter( (j) => j.status === BatchJobStatus.COMPLETED ).length; const failedJobs = jobs.filter( (j) => j.status === BatchJobStatus.FAILED ).length; const queueLength = jobs.filter( (j) => j.status === BatchJobStatus.QUEUED ).length; const completedWithTime = jobs.filter( (j) => j.status === BatchJobStatus.COMPLETED && j.result?.totalTime ); const averageProcessingTime = completedWithTime.length > 0 ? completedWithTime.reduce( (sum, job) => sum + (job.result?.totalTime || 0), 0 ) / completedWithTime.length : 0; return { totalJobs: jobs.length, activeJobs, completedJobs, failedJobs, averageProcessingTime, queueLength, }; } /** * 완료된 작업 정리 */ async cleanupCompletedJobs(olderThanHours: number = 24): Promise<number> { const cutoffTime = new Date(Date.now() - olderThanHours * 60 * 60 * 1000); let cleanedCount = 0; for (const [jobId, job] of this.jobs.entries()) { if ( (job.status === BatchJobStatus.COMPLETED || job.status === BatchJobStatus.FAILED) && job.completedAt && job.completedAt < cutoffTime ) { this.jobs.delete(jobId); cleanedCount++; } } return cleanedCount; } } // 싱글톤 인스턴스 let batchProcessorInstance: BatchProcessor | null = null; export function getBatchProcessor(): BatchProcessor { if (!batchProcessorInstance) { batchProcessorInstance = new BatchProcessor(); } return batchProcessorInstance; }