import React, { useState, useEffect } from 'react'; import { Settings, WifiOff, ArrowRight, ArrowLeft, BookOpen, Download, MousePointerClick } 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 { CreateFolderModal, RenameModal, DeleteModal } from './components/FileActionModals'; import { generateLocalFiles } from './utils/mockData'; import { generateRemoteFileList, generateServerMessage } from './services/gemini'; const App: React.FC = () => { // --- State --- const [connection, setConnection] = useState({ host: 'ftp.example.com', user: 'admin', pass: '••••••••', port: '21', protocol: 'ftp' as 'ftp' | 'sftp', passive: true, connected: false, connecting: false }); const [logs, setLogs] = useState([]); const [queue, setQueue] = useState([]); const [showSettings, setShowSettings] = useState(false); const [showSiteManager, setShowSiteManager] = useState(false); const [savedSites, setSavedSites] = useState([]); // Modals State const [activeModal, setActiveModal] = useState<'create' | 'rename' | 'delete' | null>(null); const [modalTargetIsLocal, setModalTargetIsLocal] = useState(true); // For Rename const [renameTargetName, setRenameTargetName] = useState(''); // Local File System const [local, setLocal] = useState({ 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()); // --- Helpers --- const addLog = (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)); }; // --- Backend Download Logic (Updated for SFTP Support) --- const handleDownloadBackend = () => { const backendCode = `/** * WebZilla 백엔드 프록시 서버 (Node.js) v2.0 * 지원: FTP (basic-ftp), SFTP (ssh2-sftp-client) * * 실행 방법: * 1. Node.js 설치 * 2. npm install ws basic-ftp ssh2-sftp-client * 3. node backend_proxy.js */ const WebSocket = require('ws'); const ftp = require('basic-ftp'); const SftpClient = require('ssh2-sftp-client'); const fs = require('fs'); const path = require('path'); const os = require('os'); // --- 설정 디렉토리 (AppData) --- function getConfigDir() { const homedir = os.homedir(); if (process.platform === 'win32') { return path.join(process.env.APPDATA || path.join(homedir, 'AppData', 'Roaming'), 'WebZilla'); } else if (process.platform === 'darwin') { return path.join(homedir, 'Library', 'Application Support', 'WebZilla'); } else { return path.join(homedir, '.config', 'webzilla'); } } const configDir = getConfigDir(); if (!fs.existsSync(configDir)) { try { fs.mkdirSync(configDir, { recursive: true }); } catch (e) {} } const wss = new WebSocket.Server({ port: 8080 }); console.log("🚀 WebZilla Proxy Server running on ws://localhost:8080"); wss.on('connection', (ws) => { // 클라이언트 상태 관리 let ftpClient = new ftp.Client(); let sftpClient = new SftpClient(); let currentProtocol = 'ftp'; // 'ftp' or 'sftp' let isConnected = false; ws.on('message', async (message) => { try { const data = JSON.parse(message); // --- CONNECT --- if (data.command === 'CONNECT') { currentProtocol = data.protocol || 'ftp'; const { host, port, user, pass, passive } = data; console.log(\`[\${currentProtocol.toUpperCase()}] Connecting to \${user}@\${host}:\${port}\`); try { if (currentProtocol === 'sftp') { await sftpClient.connect({ host, port: parseInt(port) || 22, username: user, password: pass }); ws.send(JSON.stringify({ type: 'status', status: 'connected', message: 'SFTP Connected' })); } else { // FTP await ftpClient.access({ host, user, password: pass, port: parseInt(port) || 21, secure: false }); // 패시브 모드는 basic-ftp에서 기본적으로 자동 처리하지만 명시적 설정이 필요할 수 있음 ws.send(JSON.stringify({ type: 'status', status: 'connected', message: 'FTP Connected' })); } isConnected = true; } catch (err) { console.error("Connect Error:", err.message); ws.send(JSON.stringify({ type: 'error', message: err.message })); } } // --- LIST --- else if (data.command === 'LIST') { if (!isConnected) return; const listPath = data.path || (currentProtocol === 'sftp' ? '.' : '/'); try { let files = []; if (currentProtocol === 'sftp') { const list = await sftpClient.list(listPath); files = list.map(f => ({ name: f.name, type: f.type === 'd' ? 'FOLDER' : 'FILE', size: f.size, date: new Date(f.modifyTime).toISOString(), permissions: f.rights ? f.rights.user + f.rights.group + f.rights.other : '' })); } else { const list = await ftpClient.list(listPath); files = list.map(f => ({ name: f.name, type: f.isDirectory ? 'FOLDER' : 'FILE', size: f.size, date: f.rawModifiedAt || new Date().toISOString(), permissions: '-' })); } ws.send(JSON.stringify({ type: 'list', files, path: listPath })); } catch (err) { ws.send(JSON.stringify({ type: 'error', message: err.message })); } } // --- MKD (Make Directory) --- else if (data.command === 'MKD') { if (!isConnected) return; try { const targetPath = data.path; // Full path if (currentProtocol === 'sftp') { await sftpClient.mkdir(targetPath, true); } else { await ftpClient.ensureDir(targetPath); // ensureDir is safer } ws.send(JSON.stringify({ type: 'success', message: 'Directory created' })); } catch(err) { ws.send(JSON.stringify({ type: 'error', message: err.message })); } } // --- DELE / RMD (Delete) --- else if (data.command === 'DELE') { if (!isConnected) return; // Note: handling single file deletion for simplicity in this snippet // In real app, iterate over items try { const { path, isFolder } = data; if (currentProtocol === 'sftp') { if (isFolder) await sftpClient.rmdir(path, true); else await sftpClient.delete(path); } else { if (isFolder) await ftpClient.removeDir(path); else await ftpClient.remove(path); } ws.send(JSON.stringify({ type: 'success', message: 'Deleted successfully' })); } catch(err) { ws.send(JSON.stringify({ type: 'error', message: err.message })); } } // --- RNFR / RNTO (Rename) --- else if (data.command === 'RENAME') { if (!isConnected) return; try { const { from, to } = data; if (currentProtocol === 'sftp') { await sftpClient.rename(from, to); } else { await ftpClient.rename(from, to); } ws.send(JSON.stringify({ type: 'success', message: 'Renamed successfully' })); } catch(err) { ws.send(JSON.stringify({ type: 'error', message: err.message })); } } // --- DISCONNECT --- else if (data.command === 'DISCONNECT') { if (currentProtocol === 'sftp') { await sftpClient.end(); } else { ftpClient.close(); } isConnected = false; ws.send(JSON.stringify({ type: 'status', status: 'disconnected' })); } // --- SAVE_SITE (Local Config) --- else if (data.command === 'SAVE_SITE') { const sitesFile = path.join(configDir, 'sites.json'); let sites = []; if (fs.existsSync(sitesFile)) sites = JSON.parse(fs.readFileSync(sitesFile, 'utf8')); // Simple append/replace logic omitted for brevity sites.push(data.siteInfo); fs.writeFileSync(sitesFile, JSON.stringify(sites, null, 2)); ws.send(JSON.stringify({ type: 'success', message: 'Site saved locally' })); } } catch (err) { console.error(err); } }); ws.on('close', () => { if (isConnected) { if (currentProtocol === 'sftp') sftpClient.end(); else ftpClient.close(); } }); }); `; const blob = new Blob([backendCode], { type: 'text/javascript' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'backend_proxy.js'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); addLog('info', '업데이트된 백엔드 코드(SFTP 지원)가 다운로드되었습니다.'); }; // --- Effects --- // Initial Load useEffect(() => { setLocal(prev => ({ ...prev, files: generateLocalFiles('/') })); addLog('info', 'WebZilla 클라이언트 v1.1.0 초기화됨'); const storedSites = localStorage.getItem('webzilla_sites'); if (storedSites) { try { setSavedSites(JSON.parse(storedSites)); } catch (e) { console.error("Failed to load sites", e); } } }, []); // Transfer Simulation Loop useEffect(() => { const interval = setInterval(() => { setQueue(prevQueue => { let hasChanges = false; const newQueue = prevQueue.map(item => { if (item.status === 'transferring') { hasChanges = true; const step = 5 + Math.random() * 10; const newProgress = Math.min(100, item.progress + step); if (newProgress === 100) { addLog('success', `전송 완료: ${item.filename}`); return { ...item, progress: 100, status: 'completed' as const, speed: '0 KB/s' }; } return { ...item, progress: newProgress, speed: `${(Math.random() * 500 + 100).toFixed(1)} KB/s` }; } return item; }); // Start next queued item const activeCount = newQueue.filter(i => i.status === 'transferring').length; if (activeCount < 2) { const nextItem = newQueue.find(i => i.status === 'queued'); if (nextItem) { hasChanges = true; nextItem.status = 'transferring'; addLog('info', `전송 시작: ${nextItem.filename}`); } } return hasChanges ? newQueue : prevQueue; }); }, 500); return () => clearInterval(interval); }, []); // --- Connection Handlers --- const handleConnect = async () => { if (connection.connected) { setConnection(prev => ({ ...prev, connected: false })); addLog('info', '서버 연결이 해제되었습니다.'); setRemote(prev => ({ ...prev, files: [], path: '/' })); setSelectedRemoteIds(new Set()); return; } setConnection(prev => ({ ...prev, connecting: true })); addLog('command', `CONNECT [${connection.protocol.toUpperCase()}] ${connection.user}@${connection.host}:${connection.port} ${connection.passive ? '(Passive)' : ''}`); // Simulation delays setTimeout(() => addLog('info', `주소 해석 중: ${connection.host}`), 500); setTimeout(() => addLog('success', '연결 성공, 환영 메시지 대기 중...'), 1500); // AI generated content setTimeout(async () => { const welcomeMsg = await generateServerMessage(connection.host); addLog('response', welcomeMsg); if (connection.protocol === 'ftp') { addLog('command', 'USER ' + connection.user); addLog('response', '331 Password required'); addLog('command', 'PASS ******'); addLog('response', '230 Logged on'); } else { addLog('info', 'SSH2 인증 성공 (SFTP)'); } setRemote(prev => ({ ...prev, isLoading: true })); const files = await generateRemoteFileList(connection.host, '/'); setRemote({ path: '/', files: files, isLoading: false }); setConnection(prev => ({ ...prev, connected: true, connecting: false })); addLog('success', '디렉토리 목록 조회 완료'); }, 2500); }; const handleRemoteNavigate = async (path: string) => { if (!connection.connected) return; setRemote(prev => ({ ...prev, isLoading: true })); setSelectedRemoteIds(new Set()); // Clear selection addLog('command', `CWD ${path}`); // Simulate network latency setTimeout(async () => { addLog('response', '250 Directory successfully changed.'); addLog('command', 'LIST'); const files = await generateRemoteFileList(connection.host, path); setRemote({ path, files, isLoading: false }); addLog('success', '디렉토리 목록 조회 완료'); }, 800); }; const handleLocalNavigate = (path: string) => { setLocal(prev => ({ ...prev, isLoading: true })); setSelectedLocalIds(new Set()); // Clear selection setTimeout(() => { setLocal({ path, files: generateLocalFiles(path), isLoading: false }); }, 200); }; // --- File Action Handlers (Triggers Modals) --- 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; const selectedIds = isLocal ? selectedLocalIds : selectedRemoteIds; if (selectedIds.size === 0) return; setModalTargetIsLocal(isLocal); setActiveModal('delete'); }; // --- Modal Confirm Callbacks --- const handleCreateFolderConfirm = (name: string) => { if (!name.trim()) return; const isLocal = modalTargetIsLocal; const newItem: FileItem = { id: `new-${Date.now()}`, name, type: FileType.FOLDER, size: 0, date: new Date().toISOString(), permissions: 'drwxr-xr-x' }; if (isLocal) { setLocal(prev => ({ ...prev, files: [...prev.files, newItem] })); addLog('info', `[로컬] 디렉토리 생성: ${name}`); } else { setRemote(prev => ({ ...prev, files: [...prev.files, newItem] })); addLog('command', `MKD ${name}`); addLog('success', `257 "${name}" created`); } setActiveModal(null); }; const handleRenameConfirm = (newName: string) => { if (!newName.trim()) return; const isLocal = modalTargetIsLocal; const selectedIds = isLocal ? selectedLocalIds : selectedRemoteIds; const files = isLocal ? local.files : remote.files; const targetFile = files.find(f => selectedIds.has(f.id)); if (!targetFile) return; if (targetFile.name === newName) { setActiveModal(null); return; } if (isLocal) { setLocal(prev => ({ ...prev, files: prev.files.map(f => f.id === targetFile.id ? { ...f, name: newName } : f) })); addLog('info', `[로컬] 이름 변경: ${targetFile.name} -> ${newName}`); } else { setRemote(prev => ({ ...prev, files: prev.files.map(f => f.id === targetFile.id ? { ...f, name: newName } : f) })); addLog('command', `RNFR ${targetFile.name}`); addLog('command', `RNTO ${newName}`); addLog('success', '250 Rename successful'); } setActiveModal(null); }; const handleDeleteConfirm = () => { const isLocal = modalTargetIsLocal; const selectedIds = isLocal ? selectedLocalIds : selectedRemoteIds; if (isLocal) { setLocal(prev => ({ ...prev, files: prev.files.filter(f => !selectedIds.has(f.id)) })); setSelectedLocalIds(new Set()); addLog('info', `[로컬] ${selectedIds.size}개 항목 삭제됨`); } else { setRemote(prev => ({ ...prev, files: prev.files.filter(f => !selectedIds.has(f.id)) })); setSelectedRemoteIds(new Set()); addLog('command', `DELE [${selectedIds.size} items]`); addLog('success', '250 Deleted successfully'); } setActiveModal(null); }; // --- Transfer Logic --- const handleUpload = () => { if (selectedLocalIds.size === 0 || !connection.connected) return; const selectedFiles = getSelectedFiles(local.files, selectedLocalIds); const filesToUpload = selectedFiles.filter(f => f.type !== FileType.FOLDER); if (filesToUpload.length === 0) { addLog('error', '업로드할 파일이 없습니다 (폴더 제외)'); return; } const newItems: TransferItem[] = filesToUpload.map(f => ({ id: Math.random().toString(), direction: 'upload', filename: f.name, progress: 0, status: 'queued', speed: '-' })); setQueue(prev => [...newItems, ...prev]); addLog('info', `${newItems.length}개 파일 업로드 대기`); }; const handleDownload = () => { if (selectedRemoteIds.size === 0 || !connection.connected) return; const selectedFiles = getSelectedFiles(remote.files, selectedRemoteIds); const filesToDownload = selectedFiles.filter(f => f.type !== FileType.FOLDER); if (filesToDownload.length === 0) { addLog('error', '다운로드할 파일이 없습니다 (폴더 제외)'); return; } const newItems: TransferItem[] = filesToDownload.map(f => ({ id: Math.random().toString(), direction: 'download', filename: f.name, progress: 0, status: 'queued', speed: '-' })); setQueue(prev => [...newItems, ...prev]); addLog('info', `${newItems.length}개 파일 다운로드 대기`); }; // --- Site Manager Handlers --- const handleSiteConnect = (site: SiteConfig) => { setConnection({ host: site.host, port: site.port, user: site.user, pass: site.pass || '', protocol: site.protocol, passive: site.passiveMode !== false, connected: false, connecting: false }); setShowSiteManager(false); setTimeout(() => handleConnect(), 100); }; // --- Render --- // Prepare data for Delete Modal 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)} /> setShowSiteManager(false)} initialSites={savedSites} onSaveSites={(sites) => { setSavedSites(sites); localStorage.setItem('webzilla_sites', JSON.stringify(sites)); }} onConnect={handleSiteConnect} /> setActiveModal(null)} onConfirm={handleCreateFolderConfirm} /> setActiveModal(null)} onConfirm={handleRenameConfirm} /> setActiveModal(null)} onConfirm={handleDeleteConfirm} /> {/* 1. Header & Quick Connect */}
WZ
웹질라
{/* Site Manager Trigger */}
{/* Quick Connect Inputs */}
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" />
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" /> {/* Passive Mode Toggle Switch */}
{/* Connect & Options */}
{!connection.connected && ( )}
{/* 2. Log View & Sponsor Area */}
SPONSORED
GOOGLE ADSENSE
Premium Ad Space Available
{/* Pattern background for placeholder effect */}
{/* 3. Main Split View */}
{/* Local Pane */}
handleLocalNavigate(local.path.split('/').slice(0, -1).join('/') || '/')} onSelectionChange={setSelectedLocalIds} selectedIds={selectedLocalIds} connected={true} 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} onCreateFolder={() => initiateCreateFolder(false)} onDelete={() => initiateDelete(false)} onRename={() => initiateRename(false)} />
{/* 4. Queue / Status */}
{/* Footer */}
); }; export default App;