Implement Transfer Queue Enhancements, Smart Queue Clearing, and File Conflict Resolution
This commit is contained in:
104
components/ConflictModal.tsx
Normal file
104
components/ConflictModal.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AlertTriangle, X, Check, ArrowRight } from 'lucide-react';
|
||||
import { formatBytes } from '../utils/formatters';
|
||||
|
||||
interface ConflictModalProps {
|
||||
isOpen: boolean;
|
||||
sourceFile: { name: string; size: number; date: string };
|
||||
targetFile: { name: string; size: number; date: string };
|
||||
onOverwrite: (applyToAll: boolean) => void;
|
||||
onSkip: (applyToAll: boolean) => void;
|
||||
onClose: () => void; // Usually acts as cancel/skip logic if forced closed
|
||||
}
|
||||
|
||||
const ConflictModal: React.FC<ConflictModalProps> = ({
|
||||
isOpen,
|
||||
sourceFile,
|
||||
targetFile,
|
||||
onOverwrite,
|
||||
onSkip,
|
||||
onClose
|
||||
}) => {
|
||||
const [applyToAll, setApplyToAll] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-[500px] max-w-full m-4 flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-yellow-50 px-4 py-3 border-b border-yellow-100 flex justify-between items-center">
|
||||
<div className="flex items-center gap-2 text-yellow-700">
|
||||
<AlertTriangle size={20} />
|
||||
<h3 className="font-bold text-sm">파일 중복 확인 (File Conflict)</h3>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-slate-600">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 flex flex-col gap-4">
|
||||
<p className="text-slate-700 text-sm">
|
||||
대상 위치에 동일한 이름의 파일이 이미 존재합니다. 어떻게 처리하시겠습니까?
|
||||
</p>
|
||||
|
||||
<div className="bg-slate-50 border border-slate-200 rounded p-4 text-xs flex flex-col gap-3">
|
||||
{/* Target (Existing) */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex flex-col gap-1 w-1/2 pr-2 border-r border-slate-200">
|
||||
<span className="font-semibold text-slate-500">기존 파일 (Target)</span>
|
||||
<span className="font-medium text-slate-800 truncate" title={targetFile.name}>{targetFile.name}</span>
|
||||
<div className="flex gap-2 text-slate-500">
|
||||
<span>{formatBytes(targetFile.size)}</span>
|
||||
<span>{new Date(targetFile.date).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Source (New) */}
|
||||
<div className="flex flex-col gap-1 w-1/2 pl-2 text-right">
|
||||
<span className="font-semibold text-blue-600">새 파일 (Source)</span>
|
||||
<span className="font-medium text-slate-800 truncate" title={sourceFile.name}>{sourceFile.name}</span>
|
||||
<div className="flex gap-2 justify-end text-slate-500">
|
||||
<span>{formatBytes(sourceFile.size)}</span>
|
||||
<span>{new Date(sourceFile.date).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="applyToAll"
|
||||
checked={applyToAll}
|
||||
onChange={(e) => setApplyToAll(e.target.checked)}
|
||||
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="applyToAll" className="text-sm text-slate-600 cursor-pointer select-none">
|
||||
이후 모든 중복 파일에 대해 동일하게 적용
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="bg-slate-50 px-4 py-3 border-t border-slate-200 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => onSkip(applyToAll)}
|
||||
className="px-4 py-2 bg-white border border-slate-300 rounded text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
건너뛰기 (Skip)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onOverwrite(applyToAll)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded text-sm font-medium hover:bg-blue-700 transition-colors shadow-sm"
|
||||
>
|
||||
덮어쓰기 (Overwrite)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConflictModal;
|
||||
@@ -1,34 +1,78 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { TransferItem } from '../types';
|
||||
import { ArrowUp, ArrowDown, CheckCircle, XCircle, Clock } from 'lucide-react';
|
||||
import { ArrowUp, ArrowDown, CheckCircle, XCircle, Clock, Trash2 } from 'lucide-react';
|
||||
|
||||
interface TransferQueueProps {
|
||||
queue: TransferItem[];
|
||||
onCancelAll: () => void;
|
||||
onClearCompleted: () => void;
|
||||
}
|
||||
|
||||
const TransferQueue: React.FC<TransferQueueProps> = ({ queue }) => {
|
||||
const TransferQueue: React.FC<TransferQueueProps> = ({ queue, onCancelAll, onClearCompleted }) => {
|
||||
const [activeTab, setActiveTab] = useState<'active' | 'completed'>('active');
|
||||
|
||||
const filteredQueue = queue.filter(item => {
|
||||
if (activeTab === 'active') {
|
||||
return item.status === 'queued' || item.status === 'transferring';
|
||||
} else {
|
||||
return item.status === 'completed' || item.status === 'failed';
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white border border-slate-300 rounded-lg overflow-hidden shadow-sm">
|
||||
<div className="bg-slate-50 px-3 py-2 border-b border-slate-200 flex justify-between items-center">
|
||||
<span className="text-slate-600 text-xs font-semibold uppercase tracking-wider">전송 대기열 (Queue)</span>
|
||||
<div className="text-xs text-slate-500">
|
||||
{queue.filter(i => i.status === 'transferring').length} 개 진행 중
|
||||
<div className="bg-slate-50 px-3 py-1 border-b border-slate-200 flex justify-between items-center">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
className={`px-3 py-1 text-xs font-semibold rounded-t-md transition-colors ${activeTab === 'active' ? 'bg-white text-blue-600 border-t border-l border-r border-slate-200 -mb-[1px] relative z-10' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
onClick={() => setActiveTab('active')}
|
||||
>
|
||||
전송 대기 ({queue.filter(i => i.status === 'queued' || i.status === 'transferring').length})
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-1 text-xs font-semibold rounded-t-md transition-colors ${activeTab === 'completed' ? 'bg-white text-emerald-600 border-t border-l border-r border-slate-200 -mb-[1px] relative z-10' : 'text-slate-500 hover:text-slate-700'}`}
|
||||
onClick={() => setActiveTab('completed')}
|
||||
>
|
||||
전송 완료 ({queue.filter(i => i.status === 'completed' || i.status === 'failed').length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'active' && filteredQueue.length > 0 && (
|
||||
<button
|
||||
onClick={onCancelAll}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded transition-colors"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
모두 취소
|
||||
</button>
|
||||
)}
|
||||
{activeTab === 'completed' && filteredQueue.length > 0 && (
|
||||
<button
|
||||
onClick={onClearCompleted}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-slate-500 hover:bg-slate-200 rounded transition-colors"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
목록 제거
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex-1 overflow-y-auto bg-white">
|
||||
<table className="w-full text-xs text-left border-collapse">
|
||||
<thead className="bg-slate-50 text-slate-500 sticky top-0 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="p-2 w-8"></th>
|
||||
<th className="p-2">파일명</th>
|
||||
<th className="p-2 w-24">구분</th>
|
||||
<th className="p-2 w-48">진행률</th>
|
||||
<th className="p-2 w-20">구분</th>
|
||||
<th className="p-2 w-40 text-center">요청</th>
|
||||
<th className="p-2 w-40 text-center">완료</th>
|
||||
<th className="p-2 w-32 text-center">소요</th>
|
||||
<th className="p-2 w-32">진행률</th>
|
||||
<th className="p-2 w-24 text-right">속도</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{queue.map((item) => (
|
||||
{filteredQueue.map((item) => (
|
||||
<tr key={item.id} className="border-b border-slate-100 hover:bg-slate-50 text-slate-700">
|
||||
<td className="p-2 text-center">
|
||||
{item.status === 'completed' && <CheckCircle size={14} className="text-emerald-500" />}
|
||||
@@ -38,20 +82,28 @@ const TransferQueue: React.FC<TransferQueueProps> = ({ queue }) => {
|
||||
<div className="w-3 h-3 rounded-full border-2 border-blue-500 border-t-transparent animate-spin mx-auto"></div>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2 truncate max-w-[200px] font-medium">{item.filename}</td>
|
||||
<td className="p-2 truncate max-w-[150px] font-medium" title={item.filename}>{item.filename}</td>
|
||||
<td className="p-2">
|
||||
<span className={`flex items-center gap-1 ${item.direction === 'upload' ? 'text-blue-600' : 'text-green-600'}`}>
|
||||
{item.direction === 'upload' ? <ArrowUp size={12} /> : <ArrowDown size={12} />}
|
||||
{item.direction === 'upload' ? '업로드' : '다운로드'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-2 text-center text-xs text-slate-500">
|
||||
{item.requestedAt ? new Date(item.requestedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '-'}
|
||||
</td>
|
||||
<td className="p-2 text-center text-xs text-slate-500">
|
||||
{item.completedAt ? new Date(item.completedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '-'}
|
||||
</td>
|
||||
<td className="p-2 text-center text-xs text-slate-500">
|
||||
{item.completedAt && item.requestedAt ? `${((item.completedAt - item.requestedAt) / 1000).toFixed(1)}s` : '-'}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<div className="w-full bg-slate-100 rounded-full h-2 overflow-hidden border border-slate-200">
|
||||
<div
|
||||
className={`h-full transition-all duration-200 ${
|
||||
item.status === 'completed' ? 'bg-emerald-500' :
|
||||
<div
|
||||
className={`h-full transition-all duration-200 ${item.status === 'completed' ? 'bg-emerald-500' :
|
||||
item.status === 'failed' ? 'bg-red-500' : 'bg-blue-500'
|
||||
}`}
|
||||
}`}
|
||||
style={{ width: `${item.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -59,10 +111,10 @@ const TransferQueue: React.FC<TransferQueueProps> = ({ queue }) => {
|
||||
<td className="p-2 text-right font-mono text-slate-500">{item.speed}</td>
|
||||
</tr>
|
||||
))}
|
||||
{queue.length === 0 && (
|
||||
{filteredQueue.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="p-8 text-center text-slate-400 italic">
|
||||
전송 대기열이 비어있습니다.
|
||||
<td colSpan={8} className="p-8 text-center text-slate-400 italic">
|
||||
{activeTab === 'active' ? '대기 중인 전송 파일이 없습니다.' : '완료된 전송 내역이 없습니다.'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user