@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
330 lines (286 loc) • 8.1 kB
text/typescript
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);
}