diff --git a/App.tsx b/App.tsx index 5fd064d..dd8f7bc 100644 --- a/App.tsx +++ b/App.tsx @@ -8,6 +8,7 @@ import SettingsModal from './components/SettingsModal'; import SiteManagerModal from './components/SiteManagerModal'; import HelpModal from './components/HelpModal'; import { CreateFolderModal, RenameModal, DeleteModal } from './components/FileActionModals'; +import { formatBytes } from './utils/formatters'; const App: React.FC = () => { // --- State --- @@ -262,7 +263,7 @@ const App: React.FC = () => { case 'success': addLog('success', data.message); - if (connection.connected) { + if (connectionRef.current.connected) { ws?.send(JSON.stringify({ command: 'LIST', path: remote.path })); } ws?.send(JSON.stringify({ command: 'LOCAL_LIST', path: local.path })); @@ -271,6 +272,45 @@ const App: React.FC = () => { case 'sites_list': setSavedSites(data.sites); break; + + case 'transfer_progress': + setQueue(prev => prev.map(item => { + if (item.id === data.id) { + const total = data.bytesOverall; + const current = data.bytes; + const progress = total > 0 ? Math.round((current / total) * 100) : 0; + return { ...item, progress, status: 'transferring', speed: `${formatBytes(current)} transferred` }; + } + return item; + })); + break; + + case 'transfer_success': + setQueue(prev => prev.map(item => { + if (item.id === data.id) { + return { ...item, progress: 100, status: 'completed', speed: 'Completed' }; + } + return item; + })); + addLog('success', data.message); + // Refresh lists + if (data.path) { + ws?.send(JSON.stringify({ command: 'LOCAL_LIST', path: local.path })); + if (connectionRef.current.connected) { + ws?.send(JSON.stringify({ command: 'LIST', path: remote.path })); + } + } + break; + + case 'transfer_error': + setQueue(prev => prev.map(item => { + if (item.id === data.id) { + return { ...item, status: 'failed', speed: data.message }; + } + return item; + })); + addLog('error', data.message); + break; } } catch (e) { console.error("WS Message Error", e); @@ -365,6 +405,74 @@ const App: React.FC = () => { if (wsRef.current) wsRef.current.send(JSON.stringify({ command: 'LOCAL_LIST', path })); }; + const handleDownload = () => { + if (!connection.connected || selectedRemoteIds.size === 0) return; + + selectedRemoteIds.forEach(id => { + const file = remote.files.find(f => f.id === id); + if (file && file.type === FileType.FILE) { + const transferId = `down-${Date.now()}-${Math.random()}`; + const separator = local.path.includes('\\') ? '\\' : '/'; + const cleanPath = local.path.endsWith(separator) ? local.path : local.path + separator; + const localTarget = cleanPath + file.name; + const remoteTarget = remote.path === '/' ? `/${file.name}` : `${remote.path}/${file.name}`; + + // Add to Queue + setQueue(prev => [...prev, { + id: transferId, + direction: 'download', + filename: file.name, + progress: 0, + status: 'queued', + speed: 'Pending...' + }]); + + addLog('command', `DOWNLOAD ${file.name} -> ${localTarget}`); + wsRef.current?.send(JSON.stringify({ + command: 'DOWNLOAD', + localPath: localTarget, + remotePath: remoteTarget, + transferId + })); + } + }); + setSelectedRemoteIds(new Set()); + }; + + const handleUpload = () => { + if (!connection.connected || selectedLocalIds.size === 0) return; + + selectedLocalIds.forEach(id => { + const file = local.files.find(f => f.id === id); + if (file && file.type === FileType.FILE) { + const transferId = `up-${Date.now()}-${Math.random()}`; + const separator = local.path.includes('\\') ? '\\' : '/'; + const cleanPath = local.path.endsWith(separator) ? local.path : local.path + separator; + const localSource = cleanPath + file.name; + const remoteTarget = remote.path === '/' ? `/${file.name}` : `${remote.path}/${file.name}`; + + // Add to Queue + setQueue(prev => [...prev, { + id: transferId, + direction: 'upload', + filename: file.name, + progress: 0, + status: 'queued', + speed: 'Pending...' + }]); + + addLog('command', `UPLOAD ${file.name} -> ${remoteTarget}`); + wsRef.current?.send(JSON.stringify({ + command: 'UPLOAD', + localPath: localSource, + remotePath: remoteTarget, + transferId + })); + } + }); + setSelectedLocalIds(new Set()); + }; + // --- File Action Handlers --- const initiateCreateFolder = (isLocal: boolean) => { if (!isLocal && !connection.connected) return; @@ -744,10 +852,20 @@ const App: React.FC = () => { {/* Middle Actions */}
- -
diff --git a/backend_proxy.cjs b/backend_proxy.cjs index 834e4f6..027bc2c 100644 --- a/backend_proxy.cjs +++ b/backend_proxy.cjs @@ -236,6 +236,82 @@ function startServer() { ws.send(JSON.stringify({ type: 'error', message: `삭제 실패: ${err.message}` })); } break; + + case 'DOWNLOAD': + if (client.closed) { + ws.send(JSON.stringify({ type: 'error', message: 'FTP 연결이 끊어져 있습니다.' })); + return; + } + try { + const { remotePath, localPath, transferId } = data; + + // Progress Handler + client.trackProgress(info => { + ws.send(JSON.stringify({ + type: 'transfer_progress', + id: transferId, + bytes: info.bytes, + bytesOverall: info.bytesOverall, + name: info.name + })); + }); + + await client.downloadTo(localPath, remotePath); + + client.trackProgress(); // Stop tracking + ws.send(JSON.stringify({ + type: 'transfer_success', + id: transferId, + message: '다운로드 완료', + path: localPath + })); + } catch (err) { + client.trackProgress(); // Stop tracking + ws.send(JSON.stringify({ + type: 'transfer_error', + id: data.transferId, + message: `다운로드 실패: ${err.message}` + })); + } + break; + + case 'UPLOAD': + if (client.closed) { + ws.send(JSON.stringify({ type: 'error', message: 'FTP 연결이 끊어져 있습니다.' })); + return; + } + try { + const { remotePath, localPath, transferId } = data; + + // Progress Handler + client.trackProgress(info => { + ws.send(JSON.stringify({ + type: 'transfer_progress', + id: transferId, + bytes: info.bytes, + bytesOverall: info.bytesOverall, + name: info.name + })); + }); + + await client.uploadFrom(localPath, remotePath); + + client.trackProgress(); + ws.send(JSON.stringify({ + type: 'transfer_success', + id: transferId, + message: '업로드 완료', + path: remotePath + })); + } catch (err) { + client.trackProgress(); + ws.send(JSON.stringify({ + type: 'transfer_error', + id: data.transferId, + message: `업로드 실패: ${err.message}` + })); + } + break; } } catch (err) { console.error("오류 발생:", err); diff --git a/webftp-backend.exe b/webftp-backend.exe index 029aaa2..e488ee0 100644 Binary files a/webftp-backend.exe and b/webftp-backend.exe differ