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:
thug0bin 2025-11-17 09:14:41 +09:00
parent 1f926d6b35
commit c6919abf1c
8 changed files with 1289 additions and 0 deletions

160
RDP/RDP_TOGGLE_API.md Normal file
View File

@ -0,0 +1,160 @@
# RDP Toggle API Documentation
## 개요
Proxmox VE 호스트에서 RDP/Shell 모드를 API로 전환할 수 있는 시스템입니다.
## 구성 요소
### 1. **rdp-toggle-api.py**
- FastAPI 기반 REST API 서버
- 포트: 8080
- RDP/Shell 모드 전환 제어
### 2. **rdp-toggle-web.html**
- 웹 기반 컨트롤 패널
- 실시간 상태 모니터링
- 설정 변경 인터페이스
### 3. **install-rdp-api.sh**
- 자동 설치 스크립트
- systemd 서비스 설정
## API 엔드포인트
### GET /status
현재 상태 확인
```bash
curl http://localhost:8080/status
```
### POST /toggle
모드 전환 (rdp/shell)
```bash
# RDP 모드로 전환
curl -X POST http://localhost:8080/toggle \
-H 'Content-Type: application/json' \
-d '{"mode":"rdp"}'
# Shell 모드로 전환
curl -X POST http://localhost:8080/toggle \
-H 'Content-Type: application/json' \
-d '{"mode":"shell"}'
```
### GET /config
현재 설정 확인
```bash
curl http://localhost:8080/config
```
### PUT /config
설정 업데이트
```bash
curl -X PUT http://localhost:8080/config \
-H 'Content-Type: application/json' \
-d '{
"rdp_server": "192.168.0.229",
"rdp_username": "0bin",
"rdp_password": "trajet6640"
}'
```
## 테스트 환경 설정
### RDP 서버 정보
- **서버 주소**: 192.168.0.229
- **사용자명**: 0bin
- **비밀번호**: trajet6640
- **로컬 사용자**: rdpuser
## 설치 방법
```bash
# 1. 설치 스크립트 실행
chmod +x install-rdp-api.sh
./install-rdp-api.sh
# 2. 서비스 상태 확인
systemctl status rdp-toggle-api
# 3. 웹 인터페이스 접속
# 브라우저에서 http://[PROXMOX_IP]:8080 접속
```
## 사용 시나리오
### 1. 초기 설정
```bash
# RDP 설정 구성
curl -X PUT http://localhost:8080/config \
-H 'Content-Type: application/json' \
-d '{
"rdp_server": "192.168.0.229",
"rdp_username": "0bin",
"rdp_password": "trajet6640"
}'
```
### 2. RDP 모드 활성화
```bash
curl -X POST http://localhost:8080/toggle \
-H 'Content-Type: application/json' \
-d '{"mode":"rdp"}'
```
### 3. Shell 모드로 복귀
```bash
curl -X POST http://localhost:8080/toggle \
-H 'Content-Type: application/json' \
-d '{"mode":"shell"}'
```
## 동작 원리
### RDP 모드 활성화 시
1. getty@tty1 자동 로그인 설정
2. X Window System 자동 시작
3. FreeRDP3 전체화면 실행
4. RDP 연결 자동 수립
### Shell 모드 활성화 시
1. RDP 프로세스 종료
2. X Window 종료
3. 자동 로그인 해제
4. 일반 TTY 로그인 화면 복원
## 상태 파일
- 상태 저장: `/var/lib/rdp-toggle/state.json`
- 설정 저장: `/var/lib/rdp-toggle/config.json`
## 문제 해결
### API 서버가 시작되지 않을 때
```bash
# 로그 확인
journalctl -u rdp-toggle-api -f
# Python 패키지 재설치
pip3 install --upgrade fastapi uvicorn
```
### RDP 연결이 실패할 때
```bash
# 현재 상태 확인
curl http://localhost:8080/status
# RDP 프로세스 확인
ps aux | grep xfreerdp3
```
### Shell 모드로 전환이 안 될 때
```bash
# 수동으로 RDP 종료
pkill -u rdpuser
systemctl restart getty@tty1
```
## 보안 고려사항
- API는 기본적으로 모든 IP에서 접근 가능 (0.0.0.0:8080)
- 프로덕션 환경에서는 방화벽 설정 권장
- 비밀번호는 평문으로 저장됨 (향후 암호화 필요)

95
RDP/README.md Normal file
View File

@ -0,0 +1,95 @@
# RDP Toggle API
Proxmox VE 호스트에서 RDP/Shell 모드를 API로 전환할 수 있는 시스템
## 개요
외부에서 API 호출을 통해 Proxmox 호스트의 물리적 화면을 Shell 모드와 RDP 모드로 전환할 수 있습니다.
프론트엔드에서 토글 버튼으로 화면 모드를 실시간으로 제어할 수 있습니다.
## 구성 파일
- **rdp-toggle-api.py** - FastAPI 기반 REST API 서버
- **install-rdp-api.sh** - 자동 설치 스크립트 (venv 환경)
- **requirements.txt** - Python 패키지 의존성
- **RDP_TOGGLE_API.md** - API 상세 문서
- **rdp-toggle-web.html** - 웹 기반 컨트롤 패널
- **proxmox-auto-rdp-setup.sh** - Proxmox RDP 초기 설정 스크립트
- **proxmox_auto_rdp_setup_korean.md** - 초기 설정 가이드
## 빠른 시작
```bash
# 1. 설치
chmod +x install-rdp-api.sh
./install-rdp-api.sh
# 2. 서비스 확인
systemctl status rdp-toggle-api
# 3. API 테스트
curl http://localhost:8090/status
```
## API 엔드포인트
### GET /status
현재 상태 확인
```bash
curl http://localhost:8090/status
```
### POST /toggle
모드 전환
```bash
# RDP 모드
curl -X POST http://localhost:8090/toggle \
-H 'Content-Type: application/json' \
-d '{"mode":"rdp"}'
# Shell 모드
curl -X POST http://localhost:8090/toggle \
-H 'Content-Type: application/json' \
-d '{"mode":"shell"}'
```
## 리액트 연동 예시
```jsx
const [status, setStatus] = useState(null);
const API_URL = 'http://your-proxmox-ip:8090';
// 상태 확인
const fetchStatus = async () => {
const res = await fetch(`${API_URL}/status`);
const data = await res.json();
setStatus(data);
};
// 모드 전환
const toggleMode = async (mode) => {
await fetch(`${API_URL}/toggle`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode })
});
};
```
## 설치 위치
- API 서버: `/opt/rdp-toggle-api/`
- Python 가상환경: `/opt/rdp-toggle-api/venv/`
- systemd 서비스: `/etc/systemd/system/rdp-toggle-api.service`
## 기능
- ✅ RDP ↔ Shell 모드 전환
- ✅ 실시간 상태 모니터링
- ✅ CORS 지원 (외부 접근 가능)
- ✅ venv 환경 (패키지 충돌 방지)
- ✅ systemd 서비스 (자동 시작)
## 포트
기본 포트: **8090**

62
RDP/install-rdp-api.sh Executable file
View File

@ -0,0 +1,62 @@
#!/bin/bash
# RDP Toggle API 설치 스크립트
set -e
echo "RDP Toggle API 설치 시작..."
# 설치 디렉토리 설정
INSTALL_DIR="/opt/rdp-toggle-api"
VENV_DIR="$INSTALL_DIR/venv"
# Python 및 venv 설치
apt update
apt install -y python3 python3-venv python3-pip
# 설치 디렉토리 생성
mkdir -p "$INSTALL_DIR"
# 가상환경 생성
echo "가상환경 생성 중..."
python3 -m venv "$VENV_DIR"
# 가상환경에서 패키지 설치
echo "패키지 설치 중..."
"$VENV_DIR/bin/pip" install --upgrade pip
"$VENV_DIR/bin/pip" install -r requirements.txt
# API 파일 복사
cp rdp-toggle-api.py "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR/rdp-toggle-api.py"
# systemd 서비스 생성
cat > /etc/systemd/system/rdp-toggle-api.service << EOF
[Unit]
Description=RDP Toggle API Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$INSTALL_DIR
ExecStart=$VENV_DIR/bin/python $INSTALL_DIR/rdp-toggle-api.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# 서비스 활성화 및 시작
systemctl daemon-reload
systemctl enable rdp-toggle-api.service
systemctl start rdp-toggle-api.service
echo "RDP Toggle API 설치 완료!"
echo "API 서버가 포트 8090에서 실행 중입니다."
echo ""
echo "사용 방법:"
echo " 상태 확인: curl http://localhost:8090/status"
echo " RDP 모드: curl -X POST http://localhost:8090/toggle -H 'Content-Type: application/json' -d '{\"mode\":\"rdp\"}'"
echo " Shell 모드: curl -X POST http://localhost:8090/toggle -H 'Content-Type: application/json' -d '{\"mode\":\"shell\"}'"

View File

@ -0,0 +1,212 @@
# Proxmox 9.0 자동 RDP 연결 설정 가이드
## 개요
Proxmox VE 9.0 (Debian 13 기반) 호스트가 부팅될 때 자동으로 Windows VM에 RDP로 풀스크린 연결하는 설정 가이드입니다.
**목표**: CLI 화면을 보지 않고 부팅 후 바로 RDP 화면이 풀스크린으로 표시
## 환경 정보
- **OS**: Proxmox VE 9.0.5 (Debian 13 기반)
- **RDP 대상**: ysleadersos.com:6642
- **인증정보**: doctor-03 / @flejtm301
## 전체 설정 과정
### 1단계: 필수 패키지 설치
```bash
# X 윈도우 시스템 및 관련 패키지 설치
apt update
apt install -y xorg openbox unclutter freerdp3-x11
# 설치된 패키지 확인
dpkg -l | grep -E "(xorg|openbox|freerdp)"
```
### 2단계: 사용자 계정 생성 및 설정
```bash
# rdpuser 계정 생성 (이미 존재한다면 건너뛰기)
useradd -m -s /bin/bash rdpuser
passwd rdpuser
# 사용자 홈 디렉토리 권한 설정
chown -R rdpuser:rdpuser /home/rdpuser
```
### 3단계: systemd 자동 로그인 설정
```bash
# getty@tty1 서비스 override 디렉토리 생성
mkdir -p /etc/systemd/system/getty@tty1.service.d
# override.conf 파일 생성
cat > /etc/systemd/system/getty@tty1.service.d/override.conf << 'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin rdpuser --noclear %I $TERM
Type=idle
EOF
# systemd 설정 리로드
systemctl daemon-reload
systemctl restart getty@tty1.service
```
### 4단계: 자동 X 시작 설정
```bash
# rdpuser의 .bash_profile 생성
cat > /home/rdpuser/.bash_profile << 'EOF'
# tty1에서만 X 자동 시작
if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
startx
logout
fi
EOF
# 파일 소유권 설정
chown rdpuser:rdpuser /home/rdpuser/.bash_profile
```
### 5단계: X 세션 설정 (.xinitrc)
```bash
# .xinitrc 파일 생성
cat > /home/rdpuser/.xinitrc << 'EOF'
#!/bin/bash
# 화면 절전 모드 비활성화
xset -dpms
xset s off
xset s noblank
# 마우스 커서 숨기기
unclutter -idle 0.1 -root &
# Openbox 윈도우 매니저 시작
openbox-session &
# 잠시 대기 (X 완전 초기화)
sleep 2
# FreeRDP3를 사용한 직접 RDP 연결 (풀스크린)
xfreerdp3 \
/v:ysleadersos.com:6642 \
/u:doctor-03 \
/p:"@flejtm301" \
+f \
/cert:ignore \
+dynamic-resolution \
/sound:sys:alsa \
+clipboard
# RDP 종료 시 X 세션도 종료
pkill -SIGTERM Xorg
EOF
# 실행 권한 및 소유권 설정
chmod +x /home/rdpuser/.xinitrc
chown rdpuser:rdpuser /home/rdpuser/.xinitrc
```
### 6단계: Openbox 설정 (풀스크린 최적화)
```bash
# Openbox 설정 디렉토리 생성
mkdir -p /home/rdpuser/.config/openbox
# rc.xml 설정 파일 생성 (윈도우 장식 제거, 풀스크린 강제)
cat > /home/rdpuser/.config/openbox/rc.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<openbox_config xmlns="http://openbox.org/3.4/rc">
<applications>
<application class="*">
<decor>no</decor>
<maximized>yes</maximized>
</application>
</applications>
</openbox_config>
EOF
# 디렉토리 및 파일 소유권 설정
chown -R rdpuser:rdpuser /home/rdpuser/.config
```
## 주요 문제 해결 과정
### 문제 1: 초기 Remmina 사용 시 연결 실패
- **증상**: 부팅 후 화면 깜빡임, RDP 연결되지 않음
- **원인**: Remmina가 자동 실행 환경에서 불안정
- **해결**: Remmina를 FreeRDP3로 교체
### 문제 2: .bash_profile의 exec startx 문제
- **증상**: 로그인/로그아웃 반복 루프
- **원인**: `exec startx`로 인한 세션 교체 문제
- **해결**: `exec startx``startx`로 변경하고 `logout` 추가
### 문제 3: FreeRDP3 명령어 문법 오류
- **증상**: "Unexpected keyword" 오류
- **해결**: 올바른 FreeRDP3 문법 적용
- `/cert-ignore``/cert:ignore`
- `/f``+f`
- `/dynamic-resolution``+dynamic-resolution`
- `/clipboard``+clipboard`
## 설정 파일 요약
### 핵심 설정 파일들:
1. `/etc/systemd/system/getty@tty1.service.d/override.conf` - 자동 로그인
2. `/home/rdpuser/.bash_profile` - X 자동 시작
3. `/home/rdpuser/.xinitrc` - RDP 연결 실행
4. `/home/rdpuser/.config/openbox/rc.xml` - 풀스크린 최적화
## 동작 흐름
1. **부팅 완료** → systemd가 tty1에서 rdpuser 자동 로그인
2. **로그인** → .bash_profile이 tty1에서 startx 실행
3. **X 시작** → .xinitrc가 실행됨
4. **Openbox 실행** → 윈도우 매니저 시작
5. **FreeRDP3 실행** → 풀스크린 RDP 연결
6. **RDP 종료시** → X 세션도 함께 종료
## 테스트 및 확인
### 설정 확인 명령어:
```bash
# 자동 로그인 서비스 상태 확인
systemctl status getty@tty1.service
# X 서버 실행 확인
ps aux | grep Xorg
# RDP 연결 테스트 (수동)
su - rdpuser -c "DISPLAY=:0 xfreerdp3 /v:ysleadersos.com:6642 /u:doctor-03 /p:'@flejtm301' +f /cert:ignore"
```
### 로그 확인:
```bash
# systemd 로그 확인
journalctl -u getty@tty1.service -f
# X 서버 로그 확인
cat /home/rdpuser/.local/share/xorg/Xorg.0.log
```
## 최종 결과
설정 완료 후 Proxmox 호스트를 재부팅하면:
- ✅ CLI 화면을 보지 않고 바로 RDP 화면이 표시됨
- ✅ 풀스크린 모드로 Windows VM에 자동 연결
- ✅ 사용자 개입 없이 완전 자동화된 부팅-RDP 연결
## 주의사항
1. **보안**: 패스워드가 설정 파일에 평문으로 저장됨 (운영 환경에서는 보안 강화 필요)
2. **네트워크**: RDP 대상 서버가 접근 가능한 상태여야 함
3. **백업**: 설정 변경 전 기존 설정 백업 권장
4. **권한**: 모든 설정 파일의 소유권이 rdpuser로 설정되어야 함
---
*생성일: 2025-08-24*
*작성자: Claude Code Assistant*

276
RDP/rdp-toggle-api.py Normal file
View File

@ -0,0 +1,276 @@
#!/usr/bin/env python3
"""
RDP Toggle API Server
Control RDP/Shell display mode via REST API
"""
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import subprocess
import os
import json
from typing import Optional
from datetime import datetime
import uvicorn
app = FastAPI(title="RDP Toggle API", version="1.0.0")
# CORS 설정 (외부에서 접근 가능)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 상태 저장 파일
STATE_FILE = "/var/lib/rdp-toggle/state.json"
CONFIG_FILE = "/var/lib/rdp-toggle/config.json"
# 기본 설정
DEFAULT_CONFIG = {
"rdp_server": "192.168.0.229",
"rdp_username": "0bin",
"rdp_password": "trajet6640",
"local_user": "rdpuser"
}
class ToggleRequest(BaseModel):
mode: str # "rdp" or "shell"
class ConfigUpdate(BaseModel):
rdp_server: Optional[str] = None
rdp_username: Optional[str] = None
rdp_password: Optional[str] = None
local_user: Optional[str] = None
class StatusResponse(BaseModel):
current_mode: str
rdp_active: bool
last_changed: str
config: dict
def ensure_directories():
"""필요한 디렉토리 생성"""
os.makedirs("/var/lib/rdp-toggle", exist_ok=True)
def load_state():
"""현재 상태 로드"""
if os.path.exists(STATE_FILE):
with open(STATE_FILE, 'r') as f:
return json.load(f)
return {
"current_mode": "shell",
"rdp_active": False,
"last_changed": datetime.now().isoformat()
}
def save_state(state):
"""상태 저장"""
ensure_directories()
with open(STATE_FILE, 'w') as f:
json.dump(state, f)
def load_config():
"""설정 로드"""
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
return json.load(f)
return DEFAULT_CONFIG
def save_config(config):
"""설정 저장"""
ensure_directories()
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f)
def enable_rdp():
"""RDP 모드 활성화"""
config = load_config()
# 자동 로그인 설정
subprocess.run([
"mkdir", "-p", "/etc/systemd/system/getty@tty1.service.d"
], check=False)
override_content = f"""[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin {config['local_user']} --noclear %I $TERM
Type=idle"""
with open("/etc/systemd/system/getty@tty1.service.d/override.conf", "w") as f:
f.write(override_content)
# X 자동 시작 스크립트
bash_profile = f"""if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
startx
logout
fi"""
user_home = f"/home/{config['local_user']}"
with open(f"{user_home}/.bash_profile", "w") as f:
f.write(bash_profile)
# .xinitrc 업데이트
xinitrc_content = f"""#!/bin/bash
xset -dpms
xset s off
xset s noblank
unclutter -idle 0.1 -root &
openbox-session &
sleep 2
xfreerdp3 /v:{config['rdp_server']} /u:{config['rdp_username']} /p:"{config['rdp_password']}" +f /cert:ignore +dynamic-resolution /sound:sys:alsa +clipboard
pkill -SIGTERM Xorg"""
with open(f"{user_home}/.xinitrc", "w") as f:
f.write(xinitrc_content)
subprocess.run(["chmod", "+x", f"{user_home}/.xinitrc"])
subprocess.run(["chown", f"{config['local_user']}:{config['local_user']}",
f"{user_home}/.bash_profile", f"{user_home}/.xinitrc"])
# systemd 리로드 및 getty 재시작
subprocess.run(["systemctl", "daemon-reload"])
subprocess.run(["systemctl", "restart", "getty@tty1.service"])
return True
def disable_rdp():
"""Shell 모드로 전환 (RDP 비활성화)"""
config = load_config()
# RDP 프로세스 종료
subprocess.run(["pkill", "-u", config['local_user'], "xfreerdp3"], check=False)
subprocess.run(["pkill", "-u", config['local_user'], "-f", "xinit|Xorg|openbox"], check=False)
# 자동 로그인 설정 제거
subprocess.run(["rm", "-f", "/etc/systemd/system/getty@tty1.service.d/override.conf"], check=False)
# 자동 시작 스크립트 제거
user_home = f"/home/{config['local_user']}"
subprocess.run(["rm", "-f", f"{user_home}/.bash_profile"], check=False)
# systemd 리로드 및 getty 재시작
subprocess.run(["systemctl", "daemon-reload"])
subprocess.run(["systemctl", "restart", "getty@tty1.service"])
# TTY1으로 전환
subprocess.run(["chvt", "1"], check=False)
return True
@app.get("/")
async def root():
"""API 정보"""
return {
"name": "RDP Toggle API",
"version": "1.0.0",
"endpoints": {
"GET /status": "현재 상태 확인",
"POST /toggle": "모드 전환 (rdp/shell)",
"GET /config": "현재 설정 확인",
"PUT /config": "설정 업데이트"
}
}
@app.get("/status", response_model=StatusResponse)
async def get_status():
"""현재 상태 반환"""
state = load_state()
config = load_config()
# 실제 프로세스 확인
try:
result = subprocess.run(
["pgrep", "-f", "xfreerdp3"],
capture_output=True,
text=True
)
rdp_running = result.returncode == 0
state["rdp_active"] = rdp_running
state["current_mode"] = "rdp" if rdp_running else "shell"
except:
pass
return StatusResponse(
current_mode=state["current_mode"],
rdp_active=state["rdp_active"],
last_changed=state["last_changed"],
config=config
)
@app.post("/toggle")
async def toggle_mode(request: ToggleRequest):
"""모드 전환"""
if request.mode not in ["rdp", "shell"]:
raise HTTPException(status_code=400, detail="Mode must be 'rdp' or 'shell'")
state = load_state()
try:
if request.mode == "rdp":
success = enable_rdp()
if success:
state["current_mode"] = "rdp"
state["rdp_active"] = True
else: # shell
success = disable_rdp()
if success:
state["current_mode"] = "shell"
state["rdp_active"] = False
state["last_changed"] = datetime.now().isoformat()
save_state(state)
return {
"status": "success",
"mode": request.mode,
"message": f"Switched to {request.mode} mode"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/config")
async def get_config():
"""현재 설정 반환"""
return load_config()
@app.put("/config")
async def update_config(update: ConfigUpdate):
"""설정 업데이트"""
config = load_config()
if update.rdp_server:
config["rdp_server"] = update.rdp_server
if update.rdp_username:
config["rdp_username"] = update.rdp_username
if update.rdp_password is not None:
config["rdp_password"] = update.rdp_password
if update.local_user:
config["local_user"] = update.local_user
save_config(config)
# 현재 RDP 모드인 경우 재시작
state = load_state()
if state["rdp_active"]:
disable_rdp()
enable_rdp()
return {
"status": "success",
"config": config
}
if __name__ == "__main__":
# Root 권한 확인
if os.geteuid() != 0:
print("This script must be run as root")
exit(1)
ensure_directories()
# 서버 시작
uvicorn.run(app, host="0.0.0.0", port=8090)

480
RDP/rdp-toggle-web.html Normal file
View 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>

4
RDP/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
fastapi==0.115.5
uvicorn==0.32.1
python-multipart==0.0.20
pydantic==2.10.3