Add RDP Toggle API with venv support
RDP 관련 파일들을 RDP 폴더로 정리하고 API 시스템 추가 주요 변경사항: - FastAPI 기반 RDP/Shell 모드 전환 API 서버 추가 - venv 환경을 사용하는 자동 설치 스크립트 - requirements.txt로 패키지 의존성 관리 - systemd 서비스로 자동 시작 설정 - CORS 지원으로 외부 프론트엔드 연동 가능 - 실시간 상태 모니터링 API - 웹 기반 컨트롤 패널 포함 파일 구성: - rdp-toggle-api.py: FastAPI REST API 서버 - install-rdp-api.sh: venv 환경 자동 설치 - requirements.txt: Python 패키지 의존성 - rdp-toggle-web.html: 웹 컨트롤 패널 - README.md: 사용 가이드 API 기능: - GET /status: 현재 모드 확인 - POST /toggle: RDP/Shell 모드 전환 - GET /config: 설정 확인 - PUT /config: 설정 업데이트 리액트 프론트엔드에서 토글로 화면 모드 제어 가능 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
480
RDP/rdp-toggle-web.html
Normal file
480
RDP/rdp-toggle-web.html
Normal file
@@ -0,0 +1,480 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RDP Toggle Control</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #f7f8fc;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.status-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.status-indicator.inactive {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.toggle-section {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.toggle-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
background: #ccc;
|
||||
border-radius: 40px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.3s;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.toggle-switch.active .toggle-slider {
|
||||
transform: translateX(40px);
|
||||
}
|
||||
|
||||
.mode-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
background: #f7f8fc;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
flex: 0 0 120px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-value {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a67d8;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e2e8f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #cbd5e0;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🖥️ RDP Toggle Control</h1>
|
||||
|
||||
<div class="alert" id="alert"></div>
|
||||
|
||||
<div class="status-card">
|
||||
<div class="status-row">
|
||||
<span class="status-label">연결 상태</span>
|
||||
<span class="status-value">
|
||||
<span class="status-indicator" id="status-indicator"></span>
|
||||
<span id="connection-status">확인 중...</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">현재 모드</span>
|
||||
<span class="status-value" id="current-mode">-</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">마지막 변경</span>
|
||||
<span class="status-value" id="last-changed">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-section">
|
||||
<div class="toggle-wrapper">
|
||||
<label class="toggle-label">
|
||||
<span>Shell</span>
|
||||
<div class="toggle-switch" id="toggle-switch">
|
||||
<div class="toggle-slider"></div>
|
||||
</div>
|
||||
<span>RDP</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<div class="config-title">RDP 설정</div>
|
||||
<div class="config-row">
|
||||
<span class="config-label">서버 주소:</span>
|
||||
<input type="text" class="config-value" id="rdp-server" placeholder="예: 192.168.0.150">
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<span class="config-label">사용자명:</span>
|
||||
<input type="text" class="config-value" id="rdp-username" placeholder="예: administrator">
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<span class="config-label">비밀번호:</span>
|
||||
<input type="password" class="config-value" id="rdp-password" placeholder="비밀번호">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-secondary" onclick="refreshStatus()">새로고침</button>
|
||||
<button class="btn btn-primary" onclick="saveConfig()">설정 저장</button>
|
||||
</div>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p style="margin-top: 10px; color: #666;">처리 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = window.location.hostname === 'localhost'
|
||||
? 'http://localhost:8090'
|
||||
: `http://${window.location.hostname}:8090`;
|
||||
|
||||
let currentMode = 'shell';
|
||||
let isUpdating = false;
|
||||
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/status`);
|
||||
if (!response.ok) throw new Error('API 연결 실패');
|
||||
|
||||
const data = await response.json();
|
||||
updateUI(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Status fetch error:', error);
|
||||
showAlert('API 서버에 연결할 수 없습니다.', 'error');
|
||||
document.getElementById('connection-status').textContent = '오프라인';
|
||||
document.getElementById('status-indicator').className = 'status-indicator inactive';
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI(data) {
|
||||
currentMode = data.current_mode;
|
||||
|
||||
// 상태 업데이트
|
||||
document.getElementById('connection-status').textContent = '온라인';
|
||||
document.getElementById('status-indicator').className = 'status-indicator active';
|
||||
document.getElementById('current-mode').textContent =
|
||||
currentMode === 'rdp' ? 'RDP 모드' : 'Shell 모드';
|
||||
|
||||
// 시간 포맷
|
||||
const lastChanged = new Date(data.last_changed);
|
||||
document.getElementById('last-changed').textContent =
|
||||
lastChanged.toLocaleString('ko-KR');
|
||||
|
||||
// 토글 스위치 업데이트
|
||||
const toggleSwitch = document.getElementById('toggle-switch');
|
||||
if (currentMode === 'rdp') {
|
||||
toggleSwitch.classList.add('active');
|
||||
} else {
|
||||
toggleSwitch.classList.remove('active');
|
||||
}
|
||||
|
||||
// 설정 값 업데이트
|
||||
if (data.config) {
|
||||
document.getElementById('rdp-server').value = data.config.rdp_server || '';
|
||||
document.getElementById('rdp-username').value = data.config.rdp_username || '';
|
||||
document.getElementById('rdp-password').value = data.config.rdp_password || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleMode() {
|
||||
if (isUpdating) return;
|
||||
|
||||
isUpdating = true;
|
||||
showLoading(true);
|
||||
|
||||
const newMode = currentMode === 'rdp' ? 'shell' : 'rdp';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ mode: newMode })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('모드 전환 실패');
|
||||
|
||||
const data = await response.json();
|
||||
showAlert(`${newMode === 'rdp' ? 'RDP' : 'Shell'} 모드로 전환되었습니다.`, 'success');
|
||||
|
||||
// 상태 새로고침
|
||||
setTimeout(() => fetchStatus(), 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Toggle error:', error);
|
||||
showAlert('모드 전환에 실패했습니다.', 'error');
|
||||
// 토글 스위치 원래대로
|
||||
const toggleSwitch = document.getElementById('toggle-switch');
|
||||
if (currentMode === 'rdp') {
|
||||
toggleSwitch.classList.add('active');
|
||||
} else {
|
||||
toggleSwitch.classList.remove('active');
|
||||
}
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
showLoading(true);
|
||||
|
||||
const config = {
|
||||
rdp_server: document.getElementById('rdp-server').value,
|
||||
rdp_username: document.getElementById('rdp-username').value,
|
||||
rdp_password: document.getElementById('rdp-password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/config`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('설정 저장 실패');
|
||||
|
||||
showAlert('설정이 저장되었습니다.', 'success');
|
||||
fetchStatus();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Config save error:', error);
|
||||
showAlert('설정 저장에 실패했습니다.', 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshStatus() {
|
||||
fetchStatus();
|
||||
showAlert('상태를 새로고침했습니다.', 'success');
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.textContent = message;
|
||||
alert.className = `alert ${type}`;
|
||||
alert.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
alert.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// 토글 스위치 이벤트
|
||||
document.getElementById('toggle-switch').addEventListener('click', toggleMode);
|
||||
|
||||
// 초기 로드
|
||||
fetchStatus();
|
||||
|
||||
// 주기적 상태 업데이트 (10초마다)
|
||||
setInterval(fetchStatus, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user