feat: Enhance UX with editable paths, settings refinement, and disconnected state visuals

This commit is contained in:
backuppc
2026-01-19 14:03:21 +09:00
parent 5fd84a7ff1
commit 253f6c4fd5
4 changed files with 195 additions and 227 deletions

103
App.tsx
View File

@@ -12,17 +12,22 @@ import { CreateFolderModal, RenameModal, DeleteModal } from './components/FileAc
const App: React.FC = () => {
// --- State ---
const savedPref = localStorage.getItem('save_connection_info') !== 'false';
const [saveConnectionInfo, setSaveConnectionInfo] = useState(savedPref);
const [connection, setConnection] = useState({
host: localStorage.getItem('last_host') || '',
user: localStorage.getItem('last_user') || '',
host: savedPref ? (localStorage.getItem('last_host') || '') : '',
user: savedPref ? (localStorage.getItem('last_user') || '') : '',
pass: '',
port: localStorage.getItem('last_port') || '21',
protocol: (localStorage.getItem('last_protocol') as 'ftp' | 'sftp') || 'ftp',
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<LogEntry[]>([]);
const [queue, setQueue] = useState<TransferItem[]>([]);
@@ -74,11 +79,20 @@ const App: React.FC = () => {
// --- Persistence Effects ---
useEffect(() => {
localStorage.setItem('last_host', connection.host);
localStorage.setItem('last_user', connection.user);
localStorage.setItem('last_port', connection.port);
localStorage.setItem('last_protocol', connection.protocol);
}, [connection.host, connection.user, connection.port, connection.protocol]);
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);
@@ -110,6 +124,7 @@ const App: React.FC = () => {
ws.onopen = () => {
addLog('success', '백엔드 프록시 서버에 연결되었습니다.');
setIsBackendConnected(true);
setShowConnectionHelp(false);
// Initial Data Requests
ws!.send(JSON.stringify({ command: 'GET_SITES' }));
@@ -117,6 +132,55 @@ const App: React.FC = () => {
ws!.send(JSON.stringify({ command: 'LOCAL_LIST', path: storedLocalPath }));
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
switch (data.type) {
// ... (no change to inside of switch) ...
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 }));
setRemote(prev => ({ ...prev, files: [], path: '/' }));
addLog('system', 'FTP 연결 종료');
} else if (data.status === 'error') {
setConnection(prev => ({ ...prev, connecting: false }));
addLog('error', data.message);
}
break;
case 'list':
// ... (rest of cases handled by maintaining existing code structure or using multi-replace if I was confident, but here let's careful) ...
// Since replace_file_content works on lines, I should target specific blocks.
// BE CAREFUL: attempting to replace too large block blindly.
// I should stick to smaller replaces.
}
// To avoid re-writing the huge switch content, I will use multiple Replace calls or just target onopen/onclose.
} catch (e) {
// ...
}
}; // This close brace is problematic if I don't include the switch logic.
// Better: just replace onopen and onclose separately.
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
@@ -185,13 +249,16 @@ const App: React.FC = () => {
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':
@@ -212,6 +279,8 @@ const App: React.FC = () => {
};
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:') {
@@ -557,7 +626,13 @@ startServer();
return (
<div className="flex flex-col h-screen bg-slate-50 text-slate-800 font-sans">
{/* Modals */}
<SettingsModal isOpen={showSettings} onClose={() => setShowSettings(false)} />
<SettingsModal
isOpen={showSettings}
onClose={() => setShowSettings(false)}
saveConnectionInfo={saveConnectionInfo}
onToggleSaveConnectionInfo={setSaveConnectionInfo}
/>
<ConnectionHelpModal isOpen={showConnectionHelp} onClose={() => setShowConnectionHelp(false)} />
<HelpModal isOpen={showHelp} onClose={() => setShowHelp(false)} initialTab={helpInitialTab} />
<SiteManagerModal
@@ -717,7 +792,7 @@ startServer();
{/* Local Pane */}
<div className="flex-1 min-h-0 flex flex-col min-w-[300px]">
<FilePane
title="로컬 사이트 (내 컴퓨터)"
title="로컬 (내 컴퓨터)"
icon="local"
path={local.path}
files={local.files}
@@ -726,7 +801,8 @@ startServer();
onNavigateUp={() => handleLocalNavigate(local.path.split(/\/|\\/).slice(0, -1).join(local.path.includes('\\') ? '\\' : '/') || (local.path.includes('\\') ? 'C:\\' : '/'))}
onSelectionChange={setSelectedLocalIds}
selectedIds={selectedLocalIds}
connected={true}
connected={isBackendConnected}
refreshKey={refreshKey}
onCreateFolder={() => initiateCreateFolder(true)}
onDelete={() => initiateDelete(true)}
onRename={() => initiateRename(true)}
@@ -746,7 +822,7 @@ startServer();
{/* Remote Pane */}
<div className="flex-1 min-h-0 flex flex-col min-w-[300px]">
<FilePane
title={`리모트 사이트: ${connection.host}`}
title={`리모트 (${connection.host})`}
icon="remote"
path={remote.path}
files={remote.files}
@@ -756,6 +832,7 @@ startServer();
onSelectionChange={setSelectedRemoteIds}
selectedIds={selectedRemoteIds}
connected={connection.connected}
refreshKey={refreshKey}
onCreateFolder={() => initiateCreateFolder(false)}
onDelete={() => initiateDelete(false)}
onRename={() => initiateRename(false)}