"use client"; import React, { useState, useEffect } from "react"; import { PlayIcon, PauseIcon, StopIcon, ChartBarIcon, PhotoIcon, } from "@heroicons/react/24/outline"; // 배치 처리 설정 interface BatchProcessingOptions { sizes: string[]; formats: string[]; quality: number; progressive: boolean; stripMetadata: boolean; enableWebp: boolean; enableAvif: boolean; } // 배치 작업 항목 interface BatchJobItem { uploadId: string; fileName: string; status: "pending" | "processing" | "completed" | "failed"; progress: number; error?: string; startedAt?: Date; completedAt?: Date; } // 배치 작업 통계 interface BatchStats { total: number; pending: number; processing: number; completed: number; failed: number; totalSizeReduction: number; averageProcessingTime: number; } interface BatchImageProcessorProps { uploadIds: string[]; onBatchComplete?: (stats: BatchStats) => void; className?: string; } export function BatchImageProcessor({ uploadIds, onBatchComplete, className = "", }: BatchImageProcessorProps) { const [jobs, setJobs] = useState([]); const [isRunning, setIsRunning] = useState(false); const [isPaused, setIsPaused] = useState(false); const [stats, setStats] = useState({ total: 0, pending: 0, processing: 0, completed: 0, failed: 0, totalSizeReduction: 0, averageProcessingTime: 0, }); const [options, setOptions] = useState({ sizes: ["thumbnail", "small", "medium", "large"], formats: ["jpeg", "webp"], quality: 80, progressive: true, stripMetadata: true, enableWebp: true, enableAvif: false, }); // 초기 작업 목록 설정 useEffect(() => { const initialJobs = uploadIds.map((uploadId) => ({ uploadId, fileName: `File ${uploadId.substring(0, 8)}...`, status: "pending" as const, progress: 0, })); setJobs(initialJobs); updateStats(initialJobs); }, [uploadIds]); // 통계 업데이트 const updateStats = (currentJobs: BatchJobItem[]) => { const newStats: BatchStats = { total: currentJobs.length, pending: currentJobs.filter((j) => j.status === "pending").length, processing: currentJobs.filter((j) => j.status === "processing").length, completed: currentJobs.filter((j) => j.status === "completed").length, failed: currentJobs.filter((j) => j.status === "failed").length, totalSizeReduction: 0, // TODO: 실제 계산 averageProcessingTime: 0, // TODO: 실제 계산 }; setStats(newStats); // 모든 작업 완료 시 콜백 호출 if ( newStats.completed + newStats.failed === newStats.total && newStats.total > 0 ) { onBatchComplete?.(newStats); } }; // 배치 처리 시작 const startBatch = async () => { setIsRunning(true); setIsPaused(false); for (let i = 0; i < jobs.length; i++) { if (isPaused) break; const job = jobs[i]; if (job.status !== "pending") continue; // 작업 상태를 processing으로 변경 setJobs((prevJobs) => { const updatedJobs = [...prevJobs]; updatedJobs[i] = { ...job, status: "processing", startedAt: new Date(), }; updateStats(updatedJobs); return updatedJobs; }); try { // 이미지 처리 요청 const response = await fetch("/api/media/process", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ uploadId: job.uploadId, inputPath: `/uploads/${job.uploadId}`, // 임시 경로 fileName: job.fileName, options, }), }); if (response.ok) { // 처리 완료 상태로 변경 setJobs((prevJobs) => { const updatedJobs = [...prevJobs]; updatedJobs[i] = { ...updatedJobs[i], status: "completed", progress: 100, completedAt: new Date(), }; updateStats(updatedJobs); return updatedJobs; }); } else { throw new Error("Processing failed"); } } catch (error) { // 실패 상태로 변경 setJobs((prevJobs) => { const updatedJobs = [...prevJobs]; updatedJobs[i] = { ...updatedJobs[i], status: "failed", error: error instanceof Error ? error.message : "Unknown error", completedAt: new Date(), }; updateStats(updatedJobs); return updatedJobs; }); } // 다음 작업 전 잠시 대기 await new Promise((resolve) => setTimeout(resolve, 500)); } setIsRunning(false); }; // 배치 처리 일시정지 const pauseBatch = () => { setIsPaused(true); setIsRunning(false); }; // 배치 처리 중지 const stopBatch = () => { setIsRunning(false); setIsPaused(false); // 모든 pending 작업을 중지 setJobs((prevJobs) => { const updatedJobs = prevJobs.map((job) => job.status === "pending" ? { ...job, status: "failed" as const, error: "Cancelled" } : job ); updateStats(updatedJobs); return updatedJobs; }); }; // 작업 재시도 const retryFailedJobs = () => { setJobs((prevJobs) => { const updatedJobs = prevJobs.map((job) => job.status === "failed" ? { ...job, status: "pending" as const, error: undefined } : job ); updateStats(updatedJobs); return updatedJobs; }); }; return (
{/* 헤더 */}

배치 이미지 처리

{!isRunning && !isPaused && ( )} {isRunning && ( )} {(isRunning || isPaused) && ( )} {stats.failed > 0 && !isRunning && ( )}
{/* 통계 */}
{stats.total}
전체
{stats.pending}
대기
{stats.processing}
처리 중
{stats.completed}
완료
{stats.failed}
실패
{/* 진행률 */}
전체 진행률 {Math.round( ((stats.completed + stats.failed) / stats.total) * 100 ) || 0} %
{/* 작업 목록 */}
{jobs.map((job, _index) => (
{job.fileName}
{job.uploadId}
{job.status === "pending" ? "대기" : job.status === "processing" ? "처리 중" : job.status === "completed" ? "완료" : "실패"}
{job.status === "processing" && (
{job.progress}%
)}
))}
{/* 처리 옵션 */}
처리 옵션
{["thumbnail", "small", "medium", "large"].map((size) => ( ))}
setOptions((prev) => ({ ...prev, quality: parseInt(e.target.value), })) } className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" />
); }