import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Settings, WifiOff, ArrowRight, ArrowLeft, BookOpen, Download, MousePointerClick, HelpCircle } from 'lucide-react'; import { FileItem, FileType, LogEntry, TransferItem, FileSystemState, SiteConfig } from './types'; import FilePane from './components/FilePane'; import LogConsole from './components/LogConsole'; import TransferQueue from './components/TransferQueue'; import SettingsModal from './components/SettingsModal'; import SiteManagerModal from './components/SiteManagerModal'; import HelpModal from './components/HelpModal'; import { CreateFolderModal, RenameModal, DeleteModal } from './components/FileActionModals'; import ConflictModal from './components/ConflictModal'; import DownloadModal from './components/DownloadModal'; import { formatBytes } from './utils/formatters'; const AdSenseBanner: React.FC = () => { useEffect(() => { try { // @ts-ignore (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) { console.error("AdSense error", e); } }, []); return (
); }; const App: React.FC = () => { // --- State --- const savedPref = localStorage.getItem('save_connection_info') !== 'false'; const [saveConnectionInfo, setSaveConnectionInfo] = useState(savedPref); const [connection, setConnection] = useState({ host: savedPref ? (localStorage.getItem('last_host') || '') : '', user: savedPref ? (localStorage.getItem('last_user') || '') : '', pass: '', port: savedPref ? (localStorage.getItem('last_port') || '21') : '21', protocol: savedPref ? ((localStorage.getItem('last_protocol') as 'ftp' | 'sftp') || 'ftp') : 'ftp', passive: true, initialPath: '', // New field for Session-specific initial path connected: false, connecting: false }); const [isBackendConnected, setIsBackendConnected] = useState(false); const [refreshKey, setRefreshKey] = useState(0); // To force FilePane input revert on error const [logs, setLogs] = useState([]); const [queue, setQueue] = useState([]); const [showSettings, setShowSettings] = useState(false); const [showConnectionHelp, setShowConnectionHelp] = useState(false); const [showSiteManager, setShowSiteManager] = useState(false); const [showHelp, setShowHelp] = useState(false); const [showDownloadModal, setShowDownloadModal] = useState(false); const [helpInitialTab, setHelpInitialTab] = useState<'sites' | 'connection' | 'files' | 'backend'>('sites'); const [savedSites, setSavedSites] = useState([]); // Modals State const [activeModal, setActiveModal] = useState<'create' | 'rename' | 'delete' | 'settings' | null>(null); const [modalTargetIsLocal, setModalTargetIsLocal] = useState(true); const [renameTargetName, setRenameTargetName] = useState(''); // Conflict Resolution State const [conflictQueue, setConflictQueue] = useState<{ source: FileItem, target: FileItem, direction: 'upload' | 'download' }[]>([]); const [isConflictModalOpen, setIsConflictModalOpen] = useState(false); const [activeConflict, setActiveConflict] = useState<{ source: FileItem, target: FileItem, direction: 'upload' | 'download' } | null>(null); const [conflictResolution, setConflictResolution] = useState<'overwrite' | 'skip' | null>(null); // Local File System const [local, setLocal] = useState({ path: localStorage.getItem('last_local_path') || '', files: [], isLoading: false }); const [selectedLocalIds, setSelectedLocalIds] = useState>(new Set()); // Remote File System const [remote, setRemote] = useState({ path: '/', files: [], isLoading: false }); const [selectedRemoteIds, setSelectedRemoteIds] = useState>(new Set()); // WebSocket const wsRef = useRef(null); const connectionRef = useRef(connection); const localRef = useRef(local); const remoteRef = useRef(remote); // --- Helpers --- const addLog = useCallback((type: LogEntry['type'], message: string) => { setLogs(prev => [...prev, { id: Math.random().toString(36), timestamp: new Date().toISOString(), type, message }]); }, []); const getSelectedFiles = (allFiles: FileItem[], ids: Set) => { return allFiles.filter(f => ids.has(f.id)); }; // --- Persistence Effects --- useEffect(() => { localStorage.setItem('save_connection_info', String(saveConnectionInfo)); if (saveConnectionInfo) { localStorage.setItem('last_host', connection.host); localStorage.setItem('last_user', connection.user); localStorage.setItem('last_port', connection.port); localStorage.setItem('last_protocol', connection.protocol); } else { localStorage.removeItem('last_host'); localStorage.removeItem('last_user'); localStorage.removeItem('last_port'); localStorage.removeItem('last_protocol'); } }, [connection.host, connection.user, connection.port, connection.protocol, saveConnectionInfo]); useEffect(() => { if (local.path) localStorage.setItem('last_local_path', local.path); }, [local.path]); // Sync connection state to ref for WS callbacks useEffect(() => { connectionRef.current = connection; localRef.current = local; remoteRef.current = remote; }, [connection, local, remote]); // --- WebSocket Setup --- useEffect(() => { let ws: WebSocket | null = null; let connectTimer: any = null; const connectWS = () => { // Debounce connection to avoid Strict Mode double-invocation issues connectTimer = setTimeout(() => { // Check if already connected/connecting to avoid dupes if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) { return; } addLog('system', '백엔드 프록시 서버(ws://localhost:8090) 연결 시도 중...'); try { ws = new WebSocket('ws://localhost:8090'); wsRef.current = ws; ws.onopen = () => { addLog('success', '백엔드 프록시 서버에 연결되었습니다.'); setIsBackendConnected(true); setShowConnectionHelp(false); // Initial Data Requests ws!.send(JSON.stringify({ command: 'GET_SITES' })); const storedLocalPath = localStorage.getItem('last_local_path') || ''; ws!.send(JSON.stringify({ command: 'LOCAL_LIST', path: storedLocalPath })); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); switch (data.type) { case 'status': if (data.status === 'connected') { setConnection(prev => ({ ...prev, connected: true, connecting: false })); addLog('success', data.message || 'FTP 연결 성공'); // Use Ref for latest state (especially initialPath from Site Manager) const currentConn = connectionRef.current; // 1. Initial Directory from Site Config if (currentConn.initialPath) { ws?.send(JSON.stringify({ command: 'LIST', path: currentConn.initialPath })); } // 2. Last Visited Path (Persistence) else { const lastRemote = localStorage.getItem(`last_remote_path_${currentConn.host}`); const initialPath = lastRemote || '/'; ws?.send(JSON.stringify({ command: 'LIST', path: initialPath })); } } else if (data.status === 'disconnected') { setConnection(prev => ({ ...prev, connected: false, connecting: false })); addLog('info', 'FTP 연결 종료'); setRemote(prev => ({ ...prev, files: [], path: '/' })); } break; case 'error': addLog('error', data.message); setConnection(prev => ({ ...prev, connecting: false })); // Check for disconnection messages if ( data.message.includes('FTP 연결이 끊어져 있습니다') || data.message.includes('NOT_CONNECTED') || data.message.includes('closed') ) { setConnection(prev => ({ ...prev, connected: false, connecting: false })); setRemote(prev => ({ ...prev, files: [], path: '/' })); } break; case 'list': const sortedRemoteFiles = data.files .map((f: any) => ({ ...f, type: f.type === 'FOLDER' ? FileType.FOLDER : FileType.FILE })) .sort((a: any, b: any) => { if (a.type !== b.type) return a.type === FileType.FOLDER ? -1 : 1; return a.name.localeCompare(b.name); }); setRemote({ path: data.path, files: sortedRemoteFiles, isLoading: false }); if (connectionRef.current.host) { localStorage.setItem(`last_remote_path_${connectionRef.current.host}`, data.path); } addLog('success', `목록 조회 완료: ${data.path}`); break; case 'local_list': const sortedLocalFiles = data.files .map((f: any) => ({ ...f, type: f.type === 'FOLDER' ? FileType.FOLDER : FileType.FILE })) .sort((a: any, b: any) => { if (a.type !== b.type) return a.type === FileType.FOLDER ? -1 : 1; return a.name.localeCompare(b.name); }); setLocal({ path: data.path, files: sortedLocalFiles, isLoading: false }); addLog('success', `[로컬] 이동 완료: ${data.path}`); break; case 'error': addLog('error', data.message); window.alert(data.message); // Show error dialog setConnection(prev => ({ ...prev, connecting: false })); setRemote(prev => ({ ...prev, isLoading: false })); setLocal(prev => ({ ...prev, isLoading: false })); setRefreshKey(prev => prev + 1); // Revert invalid inputs break; case 'success': addLog('success', data.message); if (connectionRef.current.connected) { ws?.send(JSON.stringify({ command: 'LIST', path: remoteRef.current.path })); } ws?.send(JSON.stringify({ command: 'LOCAL_LIST', path: localRef.current.path })); break; 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', completedAt: Date.now() }; } return item; })); addLog('success', data.message); // Refresh lists if (data.path) { ws?.send(JSON.stringify({ command: 'LOCAL_LIST', path: localRef.current.path })); if (connectionRef.current.connected) { ws?.send(JSON.stringify({ command: 'LIST', path: remoteRef.current.path })); } } break; case 'transfer_error': setQueue(prev => prev.map(item => { if (item.id === data.id) { return { ...item, status: 'failed', speed: data.message, completedAt: Date.now() }; } return item; })); addLog('error', data.message); break; } } catch (e) { console.error("WS Message Error", e); } }; ws.onclose = (e) => { setIsBackendConnected(false); setConnection(prev => ({ ...prev, connected: false, connecting: false })); if (e.code !== 1000) { // Not normal closure addLog('error', '백엔드 서버와 연결이 끊어졌습니다. 재연결 시도 중...'); if (window.location.protocol === 'https:') { setShowConnectionHelp(true); } // Retry loop setTimeout(() => { // Only retry if component still mounted (implied by effect cleanup) // But cleaner to just call connectWS which checks state connectWS(); }, 3000); } }; ws.onerror = (err) => { console.error("WS Error", err); addLog('error', '백엔드 소켓 연결 오류'); if (window.location.protocol === 'https:') { setShowConnectionHelp(true); } }; } catch (e) { console.error("WS Setup Error", e); if (window.location.protocol === 'https:') { setShowConnectionHelp(true); } } }, 300); }; connectWS(); return () => { clearTimeout(connectTimer); if (wsRef.current) { wsRef.current.close(1000); wsRef.current = null; } }; }, [addLog]); // Intentionally minimal deps // --- Connection Handlers --- const handleConnect = () => { if (connection.connected) { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ command: 'DISCONNECT' })); } return; } setConnection(prev => ({ ...prev, connecting: true })); addLog('command', `CONNECT [${connection.protocol.toUpperCase()}] ${connection.user}@${connection.host}:${connection.port} ${connection.passive ? '(Passive)' : ''}`); if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ command: 'CONNECT', host: connection.host, user: connection.user, pass: connection.pass, port: connection.port, protocol: connection.protocol, passive: connection.passive })); } else { addLog('error', '백엔드 서버에 연결되어 있지 않습니다. 잠시 후 다시 시도하세요.'); setConnection(prev => ({ ...prev, connecting: false })); } }; const handleRemoteNavigate = (path: string) => { if (!connection.connected) return; setRemote(prev => ({ ...prev, isLoading: true })); setSelectedRemoteIds(new Set()); if (wsRef.current) wsRef.current.send(JSON.stringify({ command: 'LIST', path })); }; const handleLocalNavigate = (path: string) => { setLocal(prev => ({ ...prev, isLoading: true })); setSelectedLocalIds(new Set()); if (wsRef.current) wsRef.current.send(JSON.stringify({ command: 'LOCAL_LIST', path })); }; const executeTransfer = (file: FileItem, direction: 'upload' | 'download', transferId: string) => { // Determine paths based on current refs (assuming context hasn't changed too much, or we validly want current dir) // Ideally we should store paths in the conflict object. For now this uses current view's path. const localP = localRef.current.path; const remoteP = remoteRef.current.path; const localSep = localP.includes('\\') ? '\\' : '/'; const localClean = localP.endsWith(localSep) ? localP : localP + localSep; const localFullPath = localClean + file.name; const remoteSep = remoteP === '/' ? '' : '/'; const remoteFullPath = `${remoteP}${remoteSep}${file.name}`; if (direction === 'upload') { addLog('command', `UPLOAD ${file.name} -> ${remoteFullPath}`); wsRef.current?.send(JSON.stringify({ command: 'UPLOAD', localPath: localFullPath, remotePath: remoteFullPath, transferId })); } else { addLog('command', `DOWNLOAD ${file.name} -> ${localFullPath}`); wsRef.current?.send(JSON.stringify({ command: 'DOWNLOAD', localPath: localFullPath, remotePath: remoteFullPath, transferId })); } }; const handleDownload = () => { if (!connection.connected || selectedRemoteIds.size === 0) return; // Filter and Process const selectedFiles = remote.files.filter(f => selectedRemoteIds.has(f.id)); const conflicts: { source: FileItem, target: FileItem, direction: 'download' }[] = []; const safeTransfers: FileItem[] = []; selectedFiles.forEach(file => { if (file.type !== FileType.FILE) return; // Check local for conflict const existing = local.files.find(f => f.name === file.name); if (existing) { conflicts.push({ source: file, target: existing, direction: 'download' }); } else { safeTransfers.push(file); } }); // Process Safe safeTransfers.forEach(file => { const transferId = `down-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; setQueue(prev => [...prev, { id: transferId, direction: 'download', filename: file.name, progress: 0, status: 'queued', speed: 'Pending...', requestedAt: Date.now() }]); executeTransfer(file, 'download', transferId); }); // Process Conflicts if (conflicts.length > 0) { if (conflictResolution === 'overwrite') { conflicts.forEach(c => { const transferId = `down-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; setQueue(prev => [...prev, { id: transferId, direction: 'download', filename: c.source.name, progress: 0, status: 'queued', speed: 'Pending...', requestedAt: Date.now() }]); executeTransfer(c.source, 'download', transferId); }); } else if (conflictResolution === 'skip') { // Do nothing } else { // Ask user setConflictQueue(prev => [...prev, ...conflicts]); } } setSelectedRemoteIds(new Set()); }; const handleUpload = () => { if (!connection.connected || selectedLocalIds.size === 0) return; const selectedFiles = local.files.filter(f => selectedLocalIds.has(f.id)); const conflicts: { source: FileItem, target: FileItem, direction: 'upload' }[] = []; const safeTransfers: FileItem[] = []; selectedFiles.forEach(file => { if (file.type !== FileType.FILE) return; const existing = remote.files.find(f => f.name === file.name); if (existing) { conflicts.push({ source: file, target: existing, direction: 'upload' }); } else { safeTransfers.push(file); } }); // Safe safeTransfers.forEach(file => { const transferId = `up-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; setQueue(prev => [...prev, { id: transferId, direction: 'upload', filename: file.name, progress: 0, status: 'queued', speed: 'Pending...', requestedAt: Date.now() }]); executeTransfer(file, 'upload', transferId); }); // Conflicts if (conflicts.length > 0) { if (conflictResolution === 'overwrite') { conflicts.forEach(c => { const transferId = `up-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; setQueue(prev => [...prev, { id: transferId, direction: 'upload', filename: c.source.name, progress: 0, status: 'queued', speed: 'Pending...', requestedAt: Date.now() }]); executeTransfer(c.source, 'upload', transferId); }); } else if (conflictResolution === 'skip') { // Skip } else { setConflictQueue(prev => [...prev, ...conflicts]); } } setSelectedLocalIds(new Set()); }; // --- File Action Handlers --- const initiateCreateFolder = (isLocal: boolean) => { if (!isLocal && !connection.connected) return; setModalTargetIsLocal(isLocal); setActiveModal('create'); }; const initiateRename = (isLocal: boolean) => { if (!isLocal && !connection.connected) return; const selectedIds = isLocal ? selectedLocalIds : selectedRemoteIds; if (selectedIds.size !== 1) return; const files = isLocal ? local.files : remote.files; const file = files.find(f => selectedIds.has(f.id)); if (file) { setRenameTargetName(file.name); setModalTargetIsLocal(isLocal); setActiveModal('rename'); } }; const initiateDelete = (isLocal: boolean) => { if (!isLocal && !connection.connected) return; setModalTargetIsLocal(isLocal); setActiveModal('delete'); }; // --- Modal Confirms --- const handleCreateFolderConfirm = (name: string) => { if (!name.trim()) return; if (modalTargetIsLocal) { addLog('command', `LOCAL_MKD ${name}`); if (wsRef.current) { // Simple path join, assuming separators are handled by Node or we use / const separator = local.path.includes('\\') ? '\\' : '/'; const cleanPath = local.path.endsWith(separator) ? local.path : local.path + separator; const targetPath = cleanPath + name; wsRef.current.send(JSON.stringify({ command: 'LOCAL_MKD', path: targetPath })); // Refresh list after short delay or wait for success response? // Success response triggers nothing specific yet, but we can hook into success message or just refresh manually here? // Better: success handler in onmessage triggers reload. // For now, let's trigger reload after delay or optimistic? // Let's rely on success message from backend to trigger reload? // Currently 'success' message just logs. // I'll add a reload request in the success handler logic or just request it here? setTimeout(() => { wsRef.current?.send(JSON.stringify({ command: 'LOCAL_LIST', path: local.path })); }, 500); } } else { addLog('command', `MKD ${name}`); if (wsRef.current) { const targetPath = remote.path === '/' ? `/${name}` : `${remote.path}/${name}`; wsRef.current.send(JSON.stringify({ command: 'MKD', path: targetPath })); setTimeout(() => { wsRef.current?.send(JSON.stringify({ command: 'LIST', path: remote.path })); }, 500); } } setActiveModal(null); }; const handleRenameConfirm = (newName: string) => { if (!newName.trim()) return; if (modalTargetIsLocal) { const selectedIds = selectedLocalIds; const file = local.files.find(f => selectedIds.has(f.id)); if (file && wsRef.current) { const separator = local.path.includes('\\') ? '\\' : '/'; const cleanPath = local.path.endsWith(separator) ? local.path : local.path + separator; const from = cleanPath + file.name; const to = cleanPath + newName; addLog('command', `LOCAL_RENAME ${file.name} -> ${newName}`); wsRef.current.send(JSON.stringify({ command: 'LOCAL_RENAME', from, to })); setTimeout(() => { wsRef.current?.send(JSON.stringify({ command: 'LOCAL_LIST', path: local.path })); }, 500); } } else { const selectedIds = selectedRemoteIds; const file = remote.files.find(f => selectedIds.has(f.id)); if (wsRef.current && file) { const from = remote.path === '/' ? `/${file.name}` : `${remote.path}/${file.name}`; const to = remote.path === '/' ? `/${newName}` : `${remote.path}/${newName}`; wsRef.current.send(JSON.stringify({ command: 'RENAME', from, to })); setTimeout(() => { wsRef.current?.send(JSON.stringify({ command: 'LIST', path: remote.path })); }, 500); } } setActiveModal(null); }; const handleDeleteConfirm = () => { if (modalTargetIsLocal) { const ids = selectedLocalIds; const fileNames = Array.from(ids).map(id => localRef.current.files.find(f => f.id === id)?.name).filter(Boolean).join(', '); addLog('command', `LOCAL_DELE ${fileNames}`); if (wsRef.current) { const separator = localRef.current.path.includes('\\') ? '\\' : '/'; const cleanPath = localRef.current.path.endsWith(separator) ? localRef.current.path : localRef.current.path + separator; ids.forEach(id => { const file = localRef.current.files.find(f => f.id === id); if (file) { const targetPath = cleanPath + file.name; wsRef.current?.send(JSON.stringify({ command: 'LOCAL_DELE', path: targetPath })); } }); setTimeout(() => { wsRef.current?.send(JSON.stringify({ command: 'LOCAL_LIST', path: localRef.current.path })); }, 500 + (ids.size * 50)); } setSelectedLocalIds(new Set()); } else { const ids = selectedRemoteIds; const fileNames = Array.from(ids).map(id => remoteRef.current.files.find(f => f.id === id)?.name).filter(Boolean).join(', '); addLog('command', `DELE ${fileNames}`); if (wsRef.current) { ids.forEach(id => { const file = remoteRef.current.files.find(f => f.id === id); if (file) { const targetPath = remoteRef.current.path === '/' ? `/${file.name}` : `${remoteRef.current.path}/${file.name}`; wsRef.current.send(JSON.stringify({ command: 'DELE', path: targetPath, isFolder: file.type === FileType.FOLDER })); } }); setTimeout(() => { wsRef.current?.send(JSON.stringify({ command: 'LIST', path: remoteRef.current.path })); }, 500 + (ids.size * 50)); } setSelectedRemoteIds(new Set()); } setActiveModal(null); }; // Conflict Processing useEffect(() => { if (conflictQueue.length > 0 && !activeConflict && !isConflictModalOpen) { // Take first const next = conflictQueue[0]; setActiveConflict(next); setIsConflictModalOpen(true); } }, [conflictQueue, activeConflict, isConflictModalOpen]); const handleConflictOverwrite = (applyToAll: boolean) => { if (!activeConflict) return; const { source, direction } = activeConflict; // Execute the transfer const transferId = `${direction === 'upload' ? 'up' : 'down'}-${Date.now()}-${source.name}`; setQueue(prev => [...prev, { id: transferId, direction, filename: source.name, progress: 0, status: 'queued', speed: '-', requestedAt: Date.now() }]); executeTransfer(source, direction, transferId); // Handle Apply to All if (applyToAll) { setConflictResolution('overwrite'); // Process remaining immediately const remaining = conflictQueue.slice(1); remaining.forEach(c => { const tid = `${c.direction === 'upload' ? 'up' : 'down'}-${Date.now()}-${c.source.name}`; setQueue(prev => [...prev, { id: tid, direction: c.direction, filename: c.source.name, progress: 0, status: 'queued', speed: '-', requestedAt: Date.now() }]); executeTransfer(c.source, c.direction, tid); }); setConflictQueue([]); } else { setConflictQueue(prev => prev.slice(1)); } setIsConflictModalOpen(false); setActiveConflict(null); }; const handleConflictSkip = (applyToAll: boolean) => { if (!activeConflict) return; if (applyToAll) { setConflictResolution('skip'); setConflictQueue([]); } else { setConflictQueue(prev => prev.slice(1)); } setIsConflictModalOpen(false); setActiveConflict(null); }; const handleCancelQueue = () => { // Keep only completed/failed items setQueue(prev => prev.filter(item => item.status === 'completed' || item.status === 'failed')); }; const handleClearCompleted = () => { // Keep only queued/transferring items setQueue(prev => prev.filter(item => item.status === 'queued' || item.status === 'transferring')); }; const lastConnectionRef = useRef<{ host: string; user: string } | null>(null); const handleSiteConnect = (site: SiteConfig) => { // Check if connecting to a different server const newConnectionKey = `${site.host}:${site.user}`; const oldConnectionKey = lastConnectionRef.current ? `${lastConnectionRef.current.host}:${lastConnectionRef.current.user}` : null; if (newConnectionKey !== oldConnectionKey) { setQueue([]); // Clear queue for new server } lastConnectionRef.current = { host: site.host, user: site.user }; // 1. Update State setConnection({ host: site.host, port: site.port, user: site.user, pass: site.pass || '', protocol: site.protocol, passive: site.passiveMode !== false, initialPath: site.initialPath || '', connected: false, connecting: true }); setShowSiteManager(false); // 2. Trigger Connection Immediately addLog('command', `CONNECT [${site.protocol.toUpperCase()}] ${site.user}@${site.host}:${site.port} ${site.passiveMode !== false ? '(Passive)' : ''}`); if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ command: 'CONNECT', host: site.host, user: site.user, pass: site.pass || '', port: site.port, protocol: site.protocol, passive: site.passiveMode !== false })); } else { addLog('error', '백엔드 서버에 연결되어 있지 않습니다. 도움말을 참고하여 백엔드를 실행하세요.'); setConnection(prev => ({ ...prev, connecting: false })); // Open Help on Backend Tab setHelpInitialTab('backend'); setShowHelp(true); } }; const handleSaveSites = (sites: SiteConfig[]) => { setSavedSites(sites); if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ command: 'SAVE_SITES', sites: sites })); } }; const getDeleteModalData = () => { const isLocal = modalTargetIsLocal; const ids = isLocal ? selectedLocalIds : selectedRemoteIds; const files = isLocal ? local.files : remote.files; const selectedFiles = files.filter(f => ids.has(f.id)); return { count: ids.size, names: selectedFiles.map(f => f.name) }; }; return (
{/* Modals */} setShowSettings(false)} saveConnectionInfo={saveConnectionInfo} onToggleSaveConnectionInfo={setSaveConnectionInfo} /> setShowHelp(false)} initialTab={helpInitialTab} /> setShowSiteManager(false)} initialSites={savedSites} onSaveSites={handleSaveSites} onConnect={handleSiteConnect} /> setActiveModal(null)} onConfirm={handleCreateFolderConfirm} /> setActiveModal(null)} onConfirm={handleRenameConfirm} /> setActiveModal(null)} onConfirm={handleDeleteConfirm} /> handleConflictSkip(false)} // Treat close as skip single /> {/* Header */}
WZ
WebFTP
setConnection(c => ({ ...c, host: e.target.value }))} className="w-48 h-[30px] bg-white border border-slate-300 rounded px-2 text-sm focus:border-blue-500 focus:outline-none placeholder:text-slate-400 shadow-sm" placeholder="ftp.example.com" />
setConnection(c => ({ ...c, user: e.target.value }))} className="w-32 h-[30px] bg-white border border-slate-300 rounded px-2 text-sm focus:border-blue-500 focus:outline-none shadow-sm placeholder:text-slate-400" placeholder="user" />
setConnection(c => ({ ...c, pass: e.target.value }))} className="w-32 h-[30px] bg-white border border-slate-300 rounded px-2 text-sm focus:border-blue-500 focus:outline-none shadow-sm" />
setConnection(c => ({ ...c, port: e.target.value }))} className="w-16 h-[30px] bg-white border border-slate-300 rounded px-2 text-sm text-center focus:border-blue-500 focus:outline-none shadow-sm" />
{!(window as any).__IS_STANDALONE__ && ( )}
{/* Log View */}
{/* Placeholder for Sponsored tag if needed, or remove it */}
SPONSORED
{/* Main Split View */}
{/* Local Pane */}
handleLocalNavigate(local.path.split(/\/|\\/).slice(0, -1).join(local.path.includes('\\') ? '\\' : '/') || (local.path.includes('\\') ? 'C:\\' : '/'))} onSelectionChange={setSelectedLocalIds} selectedIds={selectedLocalIds} connected={isBackendConnected} refreshKey={refreshKey} onCreateFolder={() => initiateCreateFolder(true)} onDelete={() => initiateDelete(true)} onRename={() => initiateRename(true)} />
{/* Middle Actions */}
{/* Remote Pane */}
handleRemoteNavigate(remote.path.split('/').slice(0, -1).join('/') || '/')} onSelectionChange={setSelectedRemoteIds} selectedIds={selectedRemoteIds} connected={connection.connected} refreshKey={refreshKey} onCreateFolder={() => initiateCreateFolder(false)} onDelete={() => initiateDelete(false)} onRename={() => initiateRename(false)} />
{/* Queue */}
{/* Download Modal - Standalone Only */} setShowDownloadModal(false)} /> {/* Footer */}
); }; export default App;