메인 콘텐츠로 건너뛰기

업로드 진행률

Dropup은 모든 업로드에 대한 상세한 진행률 추적을 제공합니다.

진행률 추적

개별 파일 진행률

각 파일에는 progress 속성(0-100)이 있습니다:

const { files } = useDropup({
upload: { url: '/api/upload' },
});

{files.map(file => (
<div key={file.id}>
<span>{file.name}</span>
{file.status === 'uploading' && (
<progress value={file.progress} max={100} />
)}
</div>
))}

전체 진행률

모든 파일의 진행률 추적:

const { state } = useDropup();

// state.progress - 모든 파일의 평균 진행률 (0-100)
// state.isUploading - 업로드가 진행 중인지 여부

<div>
{state.isUploading && (
<p>업로드 중: {state.progress}%</p>
)}
</div>

진행률 콜백

파일별 진행률

useDropup({
upload: { url: '/api/upload' },
onUploadProgress: (file, progress) => {
console.log(`${file.name}: ${progress}%`);
},
});

업로드 이벤트

useDropup({
upload: { url: '/api/upload' },

onUploadStart: (file) => {
console.log(`시작: ${file.name}`);
},

onUploadProgress: (file, progress) => {
console.log(`진행률: ${file.name} - ${progress}%`);
},

onUploadComplete: (file, response) => {
console.log(`완료: ${file.name}`);
console.log(`URL: ${file.uploadedUrl}`);
},

onUploadError: (file, error) => {
console.error(`실패: ${file.name}`, error);
},

onAllComplete: (files) => {
console.log(`모두 완료! ${files.length}개 파일 업로드됨`);
},
});

진행률 UI 구축

간단한 진행률 바

function ProgressBar({ value }: { value: number }) {
return (
<div style={{
width: '100%',
height: 8,
backgroundColor: '#e0e0e0',
borderRadius: 4,
}}>
<div style={{
width: `${value}%`,
height: '100%',
backgroundColor: '#4caf50',
borderRadius: 4,
transition: 'width 0.2s',
}} />
</div>
);
}

// 사용법
{file.status === 'uploading' && (
<ProgressBar value={file.progress} />
)}

원형 진행률

function CircularProgress({ value, size = 40 }: { value: number; size?: number }) {
const strokeWidth = 4;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const offset = circumference - (value / 100) * circumference;

return (
<svg width={size} height={size}>
<circle
stroke="#e0e0e0"
fill="transparent"
strokeWidth={strokeWidth}
r={radius}
cx={size / 2}
cy={size / 2}
/>
<circle
stroke="#4caf50"
fill="transparent"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
r={radius}
cx={size / 2}
cy={size / 2}
style={{
transform: 'rotate(-90deg)',
transformOrigin: '50% 50%',
transition: 'stroke-dashoffset 0.2s',
}}
/>
<text
x="50%"
y="50%"
textAnchor="middle"
dy=".3em"
fontSize={size / 4}
>
{value}%
</text>
</svg>
);
}

완전한 파일 업로드 카드

function FileUploadCard({ file }: { file: DropupFile }) {
const getStatusIcon = () => {
switch (file.status) {
case 'idle': return '⏳';
case 'uploading': return '📤';
case 'complete': return '✅';
case 'error': return '❌';
case 'paused': return '⏸️';
default: return '📄';
}
};

const getStatusColor = () => {
switch (file.status) {
case 'uploading': return '#2196f3';
case 'complete': return '#4caf50';
case 'error': return '#f44336';
default: return '#9e9e9e';
}
};

return (
<div style={{
display: 'flex',
alignItems: 'center',
padding: 12,
border: '1px solid #e0e0e0',
borderRadius: 8,
marginBottom: 8,
}}>
{/* 미리보기 또는 아이콘 */}
{file.preview ? (
<img
src={file.preview}
alt=""
style={{ width: 48, height: 48, objectFit: 'cover', borderRadius: 4 }}
/>
) : (
<span style={{ fontSize: 32 }}>{getStatusIcon()}</span>
)}

{/* 파일 정보 */}
<div style={{ flex: 1, marginLeft: 12 }}>
<div style={{ fontWeight: 500 }}>{file.name}</div>
<div style={{ fontSize: 12, color: '#666' }}>
{(file.size / 1024).toFixed(1)} KB
</div>

{/* 진행률 바 */}
{file.status === 'uploading' && (
<div style={{
marginTop: 4,
height: 4,
backgroundColor: '#e0e0e0',
borderRadius: 2,
}}>
<div style={{
width: `${file.progress}%`,
height: '100%',
backgroundColor: getStatusColor(),
borderRadius: 2,
}} />
</div>
)}

{/* 오류 메시지 */}
{file.error && (
<div style={{ color: '#f44336', fontSize: 12, marginTop: 4 }}>
{file.error.message}
</div>
)}
</div>

{/* 상태 배지 */}
<span style={{
padding: '4px 8px',
borderRadius: 4,
fontSize: 12,
color: 'white',
backgroundColor: getStatusColor(),
}}>
{file.status === 'uploading' ? `${file.progress}%` : file.status}
</span>
</div>
);
}

상태 카운트

업로드 상태 카운트 추적:

const { files, state } = useDropup();

const counts = {
total: files.length,
pending: files.filter(f => f.status === 'idle').length,
uploading: files.filter(f => f.status === 'uploading').length,
complete: files.filter(f => f.status === 'complete').length,
failed: files.filter(f => f.status === 'error').length,
};

// 표시
<div>
<p>전체: {counts.total}</p>
<p>업로드 중: {counts.uploading}</p>
<p>완료: {counts.complete}</p>
<p>실패: {counts.failed}</p>
</div>

업로드 속도 및 예상 시간

업로드 속도 및 예상 시간 계산:

import { useState, useRef, useEffect } from 'react';

function useUploadSpeed(file: DropupFile) {
const [speed, setSpeed] = useState(0);
const [eta, setEta] = useState<number | null>(null);
const lastProgress = useRef(0);
const lastTime = useRef(Date.now());

useEffect(() => {
if (file.status !== 'uploading') return;

const interval = setInterval(() => {
const now = Date.now();
const elapsed = (now - lastTime.current) / 1000;
const progressDiff = file.progress - lastProgress.current;
const bytesDiff = (progressDiff / 100) * file.size;

if (elapsed > 0) {
const currentSpeed = bytesDiff / elapsed;
setSpeed(currentSpeed);

const remaining = file.size * (1 - file.progress / 100);
if (currentSpeed > 0) {
setEta(remaining / currentSpeed);
}
}

lastProgress.current = file.progress;
lastTime.current = now;
}, 1000);

return () => clearInterval(interval);
}, [file.status, file.progress, file.size]);

return { speed, eta };
}

// 사용법
function FileProgress({ file }: { file: DropupFile }) {
const { speed, eta } = useUploadSpeed(file);

return (
<div>
<p>속도: {formatSpeed(speed)}</p>
<p>예상 시간: {formatTime(eta)}</p>
</div>
);
}

function formatSpeed(bytesPerSecond: number): string {
if (bytesPerSecond < 1024) return `${bytesPerSecond.toFixed(0)} B/s`;
if (bytesPerSecond < 1024 * 1024) return `${(bytesPerSecond / 1024).toFixed(1)} KB/s`;
return `${(bytesPerSecond / 1024 / 1024).toFixed(1)} MB/s`;
}

function formatTime(seconds: number | null): string {
if (!seconds) return '--';
if (seconds < 60) return `${Math.ceil(seconds)}`;
return `${Math.ceil(seconds / 60)}`;
}