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