UNPKG

@restnfeel/agentc-starter-kit

Version:

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

330 lines (286 loc) 8.1 kB
import { imageProcessor, ImageProcessingOptions, ProcessingResult, } from "./image-processor"; // 작업 상태 export type JobStatus = "pending" | "processing" | "completed" | "failed"; // 이미지 처리 작업 export interface ImageProcessingJob { id: string; uploadId: string; inputPath: string; fileName: string; options: ImageProcessingOptions; status: JobStatus; progress: number; error?: string; result?: ProcessingResult; createdAt: Date; startedAt?: Date; completedAt?: Date; } // 작업 큐 관리 class ImageProcessingQueue { private jobs: Map<string, ImageProcessingJob> = new Map(); private processing: Set<string> = new Set(); private maxConcurrentJobs: number = 3; /** * 새 처리 작업 추가 */ addJob( uploadId: string, inputPath: string, fileName: string, options: ImageProcessingOptions = {} ): string { const jobId = `job_${Date.now()}_${Math.random() .toString(36) .substring(2)}`; const job: ImageProcessingJob = { id: jobId, uploadId, inputPath, fileName, options: { generateWebP: true, generateAVIF: false, stripMetadata: true, progressive: true, optimize: true, ...options, }, status: "pending", progress: 0, createdAt: new Date(), }; this.jobs.set(jobId, job); // 즉시 처리 시작 (비동기) this.processNextJob(); return jobId; } /** * 작업 상태 조회 */ getJob(jobId: string): ImageProcessingJob | undefined { return this.jobs.get(jobId); } /** * 업로드 ID로 작업 조회 */ getJobByUploadId(uploadId: string): ImageProcessingJob | undefined { for (const job of this.jobs.values()) { if (job.uploadId === uploadId) { return job; } } return undefined; } /** * 대기 중인 작업 목록 */ getPendingJobs(): ImageProcessingJob[] { return Array.from(this.jobs.values()) .filter((job) => job.status === "pending") .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); } /** * 다음 작업 처리 */ private async processNextJob() { // 동시 처리 한도 확인 if (this.processing.size >= this.maxConcurrentJobs) { return; } const pendingJobs = this.getPendingJobs(); if (pendingJobs.length === 0) { return; } const job = pendingJobs[0]; await this.processJob(job); } /** * 개별 작업 처리 */ private async processJob(job: ImageProcessingJob) { if (this.processing.has(job.id)) { return; } this.processing.add(job.id); try { // 작업 상태 업데이트 job.status = "processing"; job.progress = 10; job.startedAt = new Date(); console.log(`Starting image processing for job ${job.id}`); // 이미지 처리 실행 const result = await imageProcessor.processImage( job.inputPath, job.fileName, job.options ); // 작업 완료 job.status = "completed"; job.progress = 100; job.result = result; job.completedAt = new Date(); console.log(`Completed image processing for job ${job.id}`); // TODO: 데이터베이스에 처리 결과 저장 await this.saveProcessingResult(job); } catch (error) { console.error(`Failed to process image for job ${job.id}:`, error); job.status = "failed"; job.error = error instanceof Error ? error.message : "Unknown error"; job.completedAt = new Date(); } finally { this.processing.delete(job.id); // 다음 작업 시작 setTimeout(() => this.processNextJob(), 100); } } /** * 처리 결과를 데이터베이스에 저장 */ private async saveProcessingResult(job: ImageProcessingJob) { if (!job.result) return; // TODO: 실제 Prisma 연결 시 구현 console.log(`Saving processing result for upload ${job.uploadId}`); // Mock implementation const mockPrisma = { upload: { update: async (data: { where: { id: string }; data: { status: string; isProcessed: boolean; optimizedUrl?: string; thumbnailUrl?: string; webpUrl?: string; width: number; height: number; aspectRatio: number; }; }) => { console.log("Updated upload with processing results:", data); return { id: job.uploadId }; }, }, mediaProcessingJob: { update: async (data: { where: { id: string }; data: { status: string; progress: number; result: string; completedAt?: Date; }; }) => { console.log("Updated processing job:", data); return { id: job.id }; }, }, }; try { // Upload 테이블 업데이트 await mockPrisma.upload.update({ where: { id: job.uploadId }, data: { status: "completed", isProcessed: true, optimizedUrl: job.result.processed[0]?.url, thumbnailUrl: job.result.processed.find((p) => p.size === "thumbnail") ?.url, webpUrl: job.result.webp?.[0]?.url, width: job.result.original.width, height: job.result.original.height, aspectRatio: job.result.original.width / job.result.original.height, }, }); // 처리 작업 상태 업데이트 await mockPrisma.mediaProcessingJob.update({ where: { id: job.id }, data: { status: "completed", progress: 100, result: JSON.stringify(job.result), completedAt: job.completedAt, }, }); } catch (error) { console.error("Failed to save processing result:", error); } } /** * 작업 취소 */ cancelJob(jobId: string): boolean { const job = this.jobs.get(jobId); if (!job || job.status !== "pending") { return false; } job.status = "failed"; job.error = "Cancelled by user"; job.completedAt = new Date(); return true; } /** * 완료된 작업 정리 */ cleanup(maxAge: number = 24 * 60 * 60 * 1000) { // 24시간 const cutoff = new Date(Date.now() - maxAge); for (const [jobId, job] of this.jobs) { if (job.completedAt && job.completedAt < cutoff) { this.jobs.delete(jobId); } } } /** * 큐 통계 */ getStats() { const jobs = Array.from(this.jobs.values()); return { total: jobs.length, pending: jobs.filter((j) => j.status === "pending").length, processing: jobs.filter((j) => j.status === "processing").length, completed: jobs.filter((j) => j.status === "completed").length, failed: jobs.filter((j) => j.status === "failed").length, concurrentJobs: this.processing.size, maxConcurrentJobs: this.maxConcurrentJobs, }; } } // 글로벌 이미지 처리 큐 export const imageProcessingQueue = new ImageProcessingQueue(); // API 헬퍼 함수들 export async function queueImageProcessing( uploadId: string, inputPath: string, fileName: string, options?: ImageProcessingOptions ): Promise<string> { return imageProcessingQueue.addJob(uploadId, inputPath, fileName, options); } export function getProcessingStatus( jobId: string ): ImageProcessingJob | undefined { return imageProcessingQueue.getJob(jobId); } export function getProcessingStatusByUpload( uploadId: string ): ImageProcessingJob | undefined { return imageProcessingQueue.getJobByUploadId(uploadId); } export function cancelProcessing(jobId: string): boolean { return imageProcessingQueue.cancelJob(jobId); } export function getQueueStats() { return imageProcessingQueue.getStats(); } // 주기적인 정리 작업 (1시간마다) if (typeof setInterval !== "undefined") { setInterval(() => { imageProcessingQueue.cleanup(); }, 60 * 60 * 1000); }