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>
480 lines
14 KiB
HTML
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> |