Compare commits
5 Commits
master
...
d63590620f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d63590620f | ||
|
|
76fb06dc31 | ||
|
|
2ce9bffb1d | ||
|
|
5074f6dc0e | ||
|
|
c5e7ec8436 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,3 +23,5 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
public/webftp-backend.exe
|
public/webftp-backend.exe
|
||||||
|
public/WebZilla.exe
|
||||||
|
public/webftp.exe
|
||||||
|
|||||||
63
App.tsx
63
App.tsx
@@ -9,8 +9,31 @@ import SiteManagerModal from './components/SiteManagerModal';
|
|||||||
import HelpModal from './components/HelpModal';
|
import HelpModal from './components/HelpModal';
|
||||||
import { CreateFolderModal, RenameModal, DeleteModal } from './components/FileActionModals';
|
import { CreateFolderModal, RenameModal, DeleteModal } from './components/FileActionModals';
|
||||||
import ConflictModal from './components/ConflictModal';
|
import ConflictModal from './components/ConflictModal';
|
||||||
|
import DownloadModal from './components/DownloadModal';
|
||||||
import { formatBytes } from './utils/formatters';
|
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 (
|
||||||
|
<div className="w-full h-full flex items-center justify-center overflow-hidden bg-slate-50">
|
||||||
|
<ins className="adsbygoogle"
|
||||||
|
style={{ display: "block", width: "100%", height: "100%" }}
|
||||||
|
data-ad-client="ca-pub-4444852135420953"
|
||||||
|
data-ad-slot="7799405796"
|
||||||
|
data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const savedPref = localStorage.getItem('save_connection_info') !== 'false';
|
const savedPref = localStorage.getItem('save_connection_info') !== 'false';
|
||||||
@@ -36,6 +59,7 @@ const App: React.FC = () => {
|
|||||||
const [showConnectionHelp, setShowConnectionHelp] = useState(false);
|
const [showConnectionHelp, setShowConnectionHelp] = useState(false);
|
||||||
const [showSiteManager, setShowSiteManager] = useState(false);
|
const [showSiteManager, setShowSiteManager] = useState(false);
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
|
const [showDownloadModal, setShowDownloadModal] = useState(false); // New state for Download Modal
|
||||||
const [helpInitialTab, setHelpInitialTab] = useState<'sites' | 'connection' | 'files' | 'backend'>('sites');
|
const [helpInitialTab, setHelpInitialTab] = useState<'sites' | 'connection' | 'files' | 'backend'>('sites');
|
||||||
const [savedSites, setSavedSites] = useState<SiteConfig[]>([]);
|
const [savedSites, setSavedSites] = useState<SiteConfig[]>([]);
|
||||||
|
|
||||||
@@ -224,7 +248,11 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
addLog('error', data.message);
|
addLog('error', data.message);
|
||||||
|
window.alert(data.message); // Show error dialog
|
||||||
setConnection(prev => ({ ...prev, connecting: false }));
|
setConnection(prev => ({ ...prev, connecting: false }));
|
||||||
|
setRemote(prev => ({ ...prev, isLoading: false }));
|
||||||
|
setLocal(prev => ({ ...prev, isLoading: false }));
|
||||||
|
setRefreshKey(prev => prev + 1); // Revert invalid inputs
|
||||||
|
|
||||||
// Check for disconnection messages
|
// Check for disconnection messages
|
||||||
if (
|
if (
|
||||||
@@ -278,14 +306,7 @@ const App: React.FC = () => {
|
|||||||
addLog('success', `[로컬] 이동 완료: ${data.path}`);
|
addLog('success', `[로컬] 이동 완료: ${data.path}`);
|
||||||
break;
|
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':
|
case 'success':
|
||||||
addLog('success', data.message);
|
addLog('success', data.message);
|
||||||
@@ -917,6 +938,10 @@ const App: React.FC = () => {
|
|||||||
onSkip={handleConflictSkip}
|
onSkip={handleConflictSkip}
|
||||||
onClose={() => handleConflictSkip(false)} // Treat close as skip single
|
onClose={() => handleConflictSkip(false)} // Treat close as skip single
|
||||||
/>
|
/>
|
||||||
|
<DownloadModal
|
||||||
|
isOpen={showDownloadModal}
|
||||||
|
onClose={() => setShowDownloadModal(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="bg-white border-b border-slate-200 p-2 shadow-sm z-20">
|
<header className="bg-white border-b border-slate-200 p-2 shadow-sm z-20">
|
||||||
@@ -1002,14 +1027,13 @@ 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>
|
||||||
|
|
||||||
<a
|
<button
|
||||||
href={`${import.meta.env.BASE_URL}webftp-backend.exe`}
|
onClick={() => setShowDownloadModal(true)}
|
||||||
download="webftp-backend.exe"
|
className="h-[30px] flex items-center justify-center px-3 rounded bg-emerald-600 hover:bg-emerald-500 text-white border border-emerald-500 shadow-md shadow-emerald-500/20 transition-all"
|
||||||
className={`h-[30px] flex items-center justify-center px-3 rounded bg-emerald-600 hover:bg-emerald-500 text-white border border-emerald-500 shadow-md shadow-emerald-500/20 transition-all ${!connection.connected ? 'animate-pulse ring-2 ring-emerald-400/50' : ''}`}
|
title="다운로드 센터"
|
||||||
title="백엔드 다운로드"
|
|
||||||
>
|
>
|
||||||
<Download size={14} className="mr-1.5" /> 백엔드
|
<Download size={14} className="mr-1.5" /> 다운로드
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowHelp(true)}
|
onClick={() => setShowHelp(true)}
|
||||||
@@ -1035,13 +1059,8 @@ const App: React.FC = () => {
|
|||||||
<div className="w-1/2 min-w-0 h-full">
|
<div className="w-1/2 min-w-0 h-full">
|
||||||
<LogConsole logs={logs} />
|
<LogConsole logs={logs} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2 min-w-0 h-full bg-white border border-slate-300 rounded-lg shadow-sm flex flex-col items-center justify-center relative overflow-hidden group cursor-pointer">
|
<div className="w-1/2 min-w-0 h-full bg-white border border-slate-300 rounded-lg shadow-sm flex flex-col items-center justify-center relative overflow-hidden">
|
||||||
<div className="absolute top-0 right-0 bg-slate-100 text-[9px] text-slate-500 px-1.5 py-0.5 rounded-bl border-b border-l border-slate-200 z-10">SPONSORED</div>
|
<AdSenseBanner />
|
||||||
<div className="text-slate-300 flex flex-col items-center gap-1 group-hover:text-slate-400 transition-colors">
|
|
||||||
<div className="font-bold text-lg tracking-widest flex items-center gap-2"><MousePointerClick size={20} /> GOOGLE ADSENSE</div>
|
|
||||||
<div className="text-xs">Premium Ad Space Available</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute inset-0 -z-0 opacity-30" style={{ backgroundImage: 'radial-gradient(#cbd5e1 1px, transparent 1px)', backgroundSize: '10px 10px' }}></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,26 @@ const path = require('path');
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
const readline = require('readline');
|
const readline = require('readline');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
// MIME Types for static serving
|
||||||
|
const MIME_TYPES = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.js': 'text/javascript',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.ttf': 'font/ttf',
|
||||||
|
'.eot': 'application/vnd.ms-fontobject',
|
||||||
|
'.otf': 'font/otf',
|
||||||
|
'.wasm': 'application/wasm'
|
||||||
|
};
|
||||||
|
|
||||||
// --- 로컬 저장소 경로 설정 (AppData 구현) ---
|
// --- 로컬 저장소 경로 설정 (AppData 구현) ---
|
||||||
function getConfigDir() {
|
function getConfigDir() {
|
||||||
@@ -38,11 +58,101 @@ if (!fs.existsSync(configDir)) {
|
|||||||
|
|
||||||
const PORT = 8090;
|
const PORT = 8090;
|
||||||
|
|
||||||
|
// --- 서버 시작 함수 (재시도 로직 포함) ---
|
||||||
// --- 서버 시작 함수 (재시도 로직 포함) ---
|
// --- 서버 시작 함수 (재시도 로직 포함) ---
|
||||||
function startServer() {
|
function startServer() {
|
||||||
const wss = new WebSocket.Server({ port: PORT });
|
const server = http.createServer((req, res) => {
|
||||||
|
// Basic Static File Server
|
||||||
|
// pkg friendly path: use path.join(__dirname, 'dist')
|
||||||
|
// When packaged with pkg, __dirname points to the virtual filesystem inside the executable
|
||||||
|
|
||||||
wss.on('error', (err) => {
|
// Special handling for executables (Download Center)
|
||||||
|
// Serve from the host filesystem (where the .exe is running)
|
||||||
|
// This prevents the need to bundle the executables inside the pkg bundle itself
|
||||||
|
if (req.url === '/webftp.exe' || req.url === '/webftp-backend.exe') {
|
||||||
|
const exeDir = path.dirname(process.execPath); // Directory of the running executable
|
||||||
|
// Fallback for dev mode (node backend_proxy.cjs) -> serve from public or current dir
|
||||||
|
const targetPath = process.pkg ? path.join(exeDir, req.url) : path.join(__dirname, 'public', req.url);
|
||||||
|
|
||||||
|
if (fs.existsSync(targetPath)) {
|
||||||
|
const stat = fs.statSync(targetPath);
|
||||||
|
|
||||||
|
// Handle HEAD request for size check
|
||||||
|
if (req.method === 'HEAD') {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Length': stat.size
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix for Windows: req.url starts with / which path.join treats as absolute
|
||||||
|
const requestPath = req.url === '/' ? '/index.html' : req.url;
|
||||||
|
// Remove leading slash for path.join to work relatively
|
||||||
|
const relativePath = requestPath.startsWith('/') ? requestPath.slice(1) : requestPath;
|
||||||
|
|
||||||
|
// Decoding URL (handling spaces etc)
|
||||||
|
const decodedPath = decodeURIComponent(relativePath);
|
||||||
|
|
||||||
|
let filePath = path.join(__dirname, 'dist', decodedPath);
|
||||||
|
|
||||||
|
// Prevent directory traversal
|
||||||
|
const distRoot = path.join(__dirname, 'dist');
|
||||||
|
if (!filePath.startsWith(distRoot)) {
|
||||||
|
console.log(`[Security Block] ${filePath} is outside ${distRoot}`);
|
||||||
|
res.writeHead(403);
|
||||||
|
res.end('Forbidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extname = path.extname(filePath);
|
||||||
|
let contentType = MIME_TYPES[extname] || 'application/octet-stream';
|
||||||
|
|
||||||
|
fs.readFile(filePath, (err, content) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
// SPA fallback: serve index.html for unknown paths (if not an asset)
|
||||||
|
if (!extname || extname === '.html') {
|
||||||
|
fs.readFile(path.join(__dirname, 'dist', 'index.html'), (err2, content2) => {
|
||||||
|
if (err2) {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('404 Not Found');
|
||||||
|
} else {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(content2, 'utf-8');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('File Not Found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(`Server Error: ${err.code}`);
|
||||||
|
}
|
||||||
|
} 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,9 +162,12 @@ function startServer() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.on('listening', () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`\n🚀 WebZilla FTP Proxy Server [v${APP_VERSION}] 가 ws://localhost:${PORT} 에서 실행 중입니다.`);
|
console.log(`\n🚀 WebFTP Proxy Server [v${APP_VERSION}] 가 http://localhost:${PORT} 에서 실행 중입니다.`);
|
||||||
console.log(`📂 설정 폴더: ${configDir}`);
|
console.log(`📂 설정 폴더: ${configDir}`);
|
||||||
|
|
||||||
|
// Open Browser
|
||||||
|
openBrowser(`http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
@@ -383,5 +496,14 @@ 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();
|
||||||
112
components/DownloadModal.tsx
Normal file
112
components/DownloadModal.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Download, Server, Package } from 'lucide-react';
|
||||||
|
import { formatBytes } from '../utils/formatters';
|
||||||
|
|
||||||
|
interface DownloadModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DownloadModal: React.FC<DownloadModalProps> = ({ isOpen, onClose }) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const baseUrl = import.meta.env.BASE_URL;
|
||||||
|
const [fileSizes, setFileSizes] = useState<{ [key: string]: string }>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const fetchSize = async (filename: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}${filename}`, { method: 'HEAD' });
|
||||||
|
const size = response.headers.get('Content-Length');
|
||||||
|
if (size) {
|
||||||
|
setFileSizes(prev => ({ ...prev, [filename]: formatBytes(parseInt(size, 10)) }));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to fetch size for ${filename}`, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSize('webftp.exe');
|
||||||
|
fetchSize('webftp-backend.exe');
|
||||||
|
}, [isOpen, baseUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/20 backdrop-blur-sm animate-in fade-in duration-200">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0"
|
||||||
|
onClick={onClose}
|
||||||
|
></div>
|
||||||
|
<div className="bg-white/90 backdrop-blur-md rounded-xl shadow-2xl border border-white/50 w-full max-w-md p-6 relative animate-in zoom-in-95 duration-200">
|
||||||
|
|
||||||
|
<h2 className="text-xl font-bold text-slate-800 mb-2 flex items-center gap-2">
|
||||||
|
<Download className="text-blue-600" />
|
||||||
|
다운로드 센터
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-500 mb-6">
|
||||||
|
사용 환경에 맞는 실행 파일을 선택하여 다운로드하세요.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<a
|
||||||
|
href={`${baseUrl}webftp.exe`}
|
||||||
|
download="webftp.exe"
|
||||||
|
className="flex items-start gap-4 p-4 rounded-lg border border-slate-200 bg-white hover:bg-blue-50 hover:border-blue-300 transition-all group shadow-sm hover:shadow-md"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-full bg-blue-100/50 flex items-center justify-center shrink-0 group-hover:bg-blue-200/50 transition-colors">
|
||||||
|
<Package className="text-blue-600 w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-bold text-slate-800 mb-1 flex items-center justify-between">
|
||||||
|
WebFTP (권장)
|
||||||
|
WebFTP (권장)
|
||||||
|
<span className="flex gap-1">
|
||||||
|
{fileSizes['webftp.exe'] && <span className="text-[10px] bg-slate-100 text-slate-500 px-1.5 py-0.5 rounded opacity-80">{fileSizes['webftp.exe']}</span>}
|
||||||
|
<span className="text-[10px] font-bold bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">ALL-IN-ONE</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed">
|
||||||
|
백엔드와 프론트엔드가 포함된 단일 실행 파일입니다. <br />
|
||||||
|
<span className="font-semibold text-slate-600">다운로드 후 바로 실행하여 사용하세요.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`${baseUrl}webftp-backend.exe`}
|
||||||
|
download="webftp-backend.exe"
|
||||||
|
className="flex items-start gap-4 p-4 rounded-lg border border-slate-200 bg-white hover:bg-slate-50 hover:border-slate-300 transition-all group shadow-sm hover:shadow-md"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-full bg-slate-100/50 flex items-center justify-center shrink-0 group-hover:bg-slate-200/50 transition-colors">
|
||||||
|
<Server className="text-slate-600 w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-bold text-slate-800 mb-1 flex items-center justify-between">
|
||||||
|
Backend Only
|
||||||
|
{fileSizes['webftp-backend.exe'] && <span className="text-[10px] bg-slate-100 text-slate-500 px-1.5 py-0.5 rounded opacity-80">{fileSizes['webftp-backend.exe']}</span>}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed">
|
||||||
|
백엔드 서버 파일만 다운로드합니다. <br />
|
||||||
|
<span className="font-semibold text-slate-600">백엔드를 실행하시면 현재 사이트에서 백엔드가 자동 연결 됩니다</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm text-slate-600 hover:text-slate-900 font-medium transition-colors"
|
||||||
|
>
|
||||||
|
닫기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadModal;
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4444852135420953"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>WebFTP by SIMP</title>
|
<title>WebFTP by SIMP</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<style>
|
<style>
|
||||||
/* Custom scrollbar for a more native app feel - Light Theme */
|
/* Custom scrollbar for a more native app feel - Light Theme */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
if (!rootElement) {
|
if (!rootElement) {
|
||||||
|
|||||||
2
make.bat
2
make.bat
@@ -1 +1 @@
|
|||||||
pkg backend_proxy.cjs --targets node18-win-x64 --output .\public\webftp-backend.exe
|
call npm run build
|
||||||
|
|||||||
2442
package-lock.json
generated
2442
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
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 --targets node18-win-x64 --output ./public/webftp-backend.exe",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"proxy": "node backend_proxy.cjs"
|
"proxy": "node backend_proxy.cjs"
|
||||||
},
|
},
|
||||||
@@ -18,10 +22,24 @@
|
|||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"autoprefixer": "^10.4.23",
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.8.1",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"rimraf": "^6.1.2",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
12
tailwind.config.js
Normal file
12
tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
"./**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
@@ -3,22 +3,22 @@ import { defineConfig, loadEnv } from 'vite';
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, '.', '');
|
const env = loadEnv(mode, '.', '');
|
||||||
return {
|
return {
|
||||||
base: '/ftp/',
|
base: './',
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
},
|
},
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
define: {
|
define: {
|
||||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, '.'),
|
'@': path.resolve(__dirname, '.'),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user