pve9-repo-fix/RDP/rdp-toggle-web.html
thug0bin c6919abf1c 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>
2025-11-17 09:14:41 +09:00

480 lines
14 KiB
HTML

<!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>