로컬실행모드추가 및 로컬실행시에는 백엔드 버튼 숨김
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
public/webftp-backend.exe
|
public/webftp-backend.exe
|
||||||
|
public/webftp.exe
|
||||||
|
|||||||
51
App.tsx
51
App.tsx
@@ -165,55 +165,6 @@ const App: React.FC = () => {
|
|||||||
ws!.send(JSON.stringify({ command: 'LOCAL_LIST', path: storedLocalPath }));
|
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) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
@@ -1024,6 +975,7 @@ const App: React.FC = () => {
|
|||||||
{connection.connecting ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : (connection.connected ? <><WifiOff size={16} /> 연결 해제</> : '빠른 연결')}
|
{connection.connecting ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : (connection.connected ? <><WifiOff size={16} /> 연결 해제</> : '빠른 연결')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{!(window as any).__IS_STANDALONE__ && (
|
||||||
<a
|
<a
|
||||||
href={`${import.meta.env.BASE_URL}webftp-backend.exe`}
|
href={`${import.meta.env.BASE_URL}webftp-backend.exe`}
|
||||||
download="webftp-backend.exe"
|
download="webftp-backend.exe"
|
||||||
@@ -1032,6 +984,7 @@ const App: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Download size={14} className="mr-1.5" /> 백엔드
|
<Download size={14} className="mr-1.5" /> 백엔드
|
||||||
</a>
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowHelp(true)}
|
onClick={() => setShowHelp(true)}
|
||||||
|
|||||||
@@ -40,9 +40,102 @@ const PORT = 8090;
|
|||||||
|
|
||||||
// --- 서버 시작 함수 (재시도 로직 포함) ---
|
// --- 서버 시작 함수 (재시도 로직 포함) ---
|
||||||
function startServer() {
|
function startServer() {
|
||||||
const wss = new WebSocket.Server({ port: PORT });
|
const http = require('http'); // HTTP server module required
|
||||||
|
|
||||||
wss.on('error', (err) => {
|
const server = http.createServer((req, res) => {
|
||||||
|
// Parse URL to handle query strings and decoding
|
||||||
|
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
const pathname = decodeURIComponent(parsedUrl.pathname);
|
||||||
|
|
||||||
|
// Special handling for executables (Download Center)
|
||||||
|
if (pathname === '/webftp.exe' || pathname === '/webftp-backend.exe') {
|
||||||
|
const exeDir = path.dirname(process.execPath);
|
||||||
|
// Fallback for dev mode -> serve from public
|
||||||
|
const targetPath = process.pkg ? path.join(exeDir, pathname) : path.join(__dirname, 'public', pathname);
|
||||||
|
|
||||||
|
if (fs.existsSync(targetPath)) {
|
||||||
|
const stat = fs.statSync(targetPath);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Length': stat.size,
|
||||||
|
'Content-Disposition': `attachment; filename="${path.basename(targetPath)}"`
|
||||||
|
});
|
||||||
|
const readStream = fs.createReadStream(targetPath);
|
||||||
|
readStream.pipe(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static File Serving
|
||||||
|
// Handle /ftp base path (strip it)
|
||||||
|
let normalizedPath = pathname;
|
||||||
|
if (normalizedPath.startsWith('/ftp')) {
|
||||||
|
normalizedPath = normalizedPath.replace(/^\/ftp/, '') || '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePath = path.join(__dirname, 'dist', normalizedPath === '/' ? 'index.html' : normalizedPath);
|
||||||
|
|
||||||
|
// Prevent traversal
|
||||||
|
if (!filePath.startsWith(path.join(__dirname, 'dist'))) {
|
||||||
|
res.writeHead(403); res.end('Forbidden'); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extname = path.extname(filePath);
|
||||||
|
// Basic MIME types
|
||||||
|
const MIME_TYPES = {
|
||||||
|
'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
|
||||||
|
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
|
||||||
|
'.svg': 'image/svg+xml', '.ico': 'image/x-icon'
|
||||||
|
};
|
||||||
|
let contentType = MIME_TYPES[extname] || 'application/octet-stream';
|
||||||
|
|
||||||
|
fs.readFile(filePath, (err, content) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
// SPA fallback
|
||||||
|
if (!extname || extname === '.html') {
|
||||||
|
fs.readFile(path.join(__dirname, 'dist', 'index.html'), (err2, content2) => {
|
||||||
|
if (err2) {
|
||||||
|
// If index.html is missing (Backend Only Mode), return 404
|
||||||
|
console.log(`[404] Backend Only Mode - Missing: ${pathname}`);
|
||||||
|
res.writeHead(404); res.end('Backend Only Mode - No Frontend Assets Found');
|
||||||
|
} else {
|
||||||
|
let html = content2.toString('utf-8');
|
||||||
|
if (process.pkg) {
|
||||||
|
html = html.replace('</head>', '<script>window.__IS_STANDALONE__ = true;</script></head>');
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`[404] File Not Found: ${pathname}`);
|
||||||
|
res.writeHead(404); res.end('File Not Found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`[500] Server Error for ${pathname}:`, err.code);
|
||||||
|
res.writeHead(500); res.end(`Server Error: ${err.code}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Determine if we need to inject script for main index.html access
|
||||||
|
if (filePath.endsWith('index.html') || extname === '.html') {
|
||||||
|
let html = content.toString('utf-8');
|
||||||
|
if (process.pkg) {
|
||||||
|
html = html.replace('</head>', '<script>window.__IS_STANDALONE__ = true;</script></head>');
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
res.end(html);
|
||||||
|
} else {
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
res.end(content, 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const wss = new WebSocket.Server({ server });
|
||||||
|
|
||||||
|
server.on('error', (err) => {
|
||||||
if (err.code === 'EADDRINUSE') {
|
if (err.code === 'EADDRINUSE') {
|
||||||
console.error(`\n❌ 포트 ${PORT}이(가) 이미 사용 중입니다.`);
|
console.error(`\n❌ 포트 ${PORT}이(가) 이미 사용 중입니다.`);
|
||||||
handlePortConflict();
|
handlePortConflict();
|
||||||
@@ -52,11 +145,21 @@ function startServer() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.on('listening', () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`\n🚀 WebZilla FTP Proxy Server [v${APP_VERSION}] 가 ws://localhost:${PORT} 에서 실행 중입니다.`);
|
console.log(`\n🚀 WebZilla Server [v${APP_VERSION}] running at http://localhost:${PORT}`);
|
||||||
console.log(`📂 설정 폴더: ${configDir}`);
|
console.log(`📂 Config: ${configDir}`);
|
||||||
|
|
||||||
|
// Check if Frontend Assets exist (dist/index.html)
|
||||||
|
const frontendExists = fs.existsSync(path.join(__dirname, 'dist', 'index.html'));
|
||||||
|
if (frontendExists) {
|
||||||
|
console.log("✨ Frontend detected. Launching browser...");
|
||||||
|
openBrowser(`http://localhost:${PORT}/ftp/`);
|
||||||
|
} else {
|
||||||
|
console.log("🔧 Backend Only Mode. Waiting for connections...");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// WSS Connection Handling (Existing Logic)
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
const client = new ftp.Client();
|
const client = new ftp.Client();
|
||||||
|
|
||||||
@@ -82,6 +185,7 @@ function startServer() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case 'LIST':
|
case 'LIST':
|
||||||
if (client.closed) {
|
if (client.closed) {
|
||||||
ws.send(JSON.stringify({ type: 'error', message: 'FTP 연결이 끊어져 있습니다.' }));
|
ws.send(JSON.stringify({ type: 'error', message: 'FTP 연결이 끊어져 있습니다.' }));
|
||||||
@@ -383,5 +487,15 @@ function askToKill(pid) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 브라우저 열기 ---
|
||||||
|
function openBrowser(url) {
|
||||||
|
const start = (process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open');
|
||||||
|
exec(`${start} ${url}`, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log("브라우저를 자동으로 열 수 없습니다:", err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 초기 실행
|
// 초기 실행
|
||||||
startServer();
|
startServer();
|
||||||
1781
package-lock.json
generated
1781
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -3,9 +3,13 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"bin": "backend_proxy.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "pkg backend_proxy.cjs --targets node18-win-x64 --output public/webftp-backend.exe && vite build",
|
"clean": "rimraf public/webftp.exe public/webftp-backend.exe dist/webftp.exe dist/webftp-backend.exe",
|
||||||
|
"build": "npm run clean && vite build && npm run build:backend && npm run build:full",
|
||||||
|
"build:full": "pkg . --output ./public/webftp.exe",
|
||||||
|
"build:backend": "pkg backend_proxy.cjs -c pkg-backend.json --output ./public/webftp-backend.exe",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"proxy": "node backend_proxy.cjs"
|
"proxy": "node backend_proxy.cjs"
|
||||||
},
|
},
|
||||||
@@ -21,7 +25,17 @@
|
|||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.8.1",
|
||||||
|
"rimraf": "^6.1.2",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
|
},
|
||||||
|
"pkg": {
|
||||||
|
"scripts": "backend_proxy.cjs",
|
||||||
|
"assets": [
|
||||||
|
"dist/**/*"
|
||||||
|
],
|
||||||
|
"targets": [
|
||||||
|
"node18-win-x64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
pkg-backend.json
Normal file
12
pkg-backend.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "webzilla-backend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"bin": "backend_proxy.cjs",
|
||||||
|
"pkg": {
|
||||||
|
"scripts": "backend_proxy.cjs",
|
||||||
|
"assets": [],
|
||||||
|
"targets": [
|
||||||
|
"node18-win-x64"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user