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:
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\"}'"

535
RDP/proxmox-auto-rdp-setup.sh Executable file
View File

@@ -0,0 +1,535 @@
#!/bin/bash
# Proxmox Auto RDP Setup Script
# 자동으로 Proxmox 호스트를 RDP VM에 연결하는 설정을 수행합니다
# 사용법: bash -c "$(curl -fsSL [스크립트 URL])"
set -euo pipefail
# 색상 코드 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# 로그 함수들
msg_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
msg_ok() {
echo -e "${GREEN}[OK]${NC} $1"
}
msg_error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}
msg_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
# 헤더 출력
print_header() {
clear
echo -e "${PURPLE}"
echo "═══════════════════════════════════════════════════════════════════"
echo " Proxmox Auto RDP Setup Script v1.0"
echo "═══════════════════════════════════════════════════════════════════"
echo -e "${NC}"
echo "이 스크립트는 Proxmox VE 호스트가 부팅 시 자동으로 RDP 연결하도록 설정합니다."
echo ""
}
# Proxmox 버전 확인
check_proxmox_version() {
msg_info "Proxmox VE 버전 확인 중..."
if [ ! -f /etc/pve/.version ]; then
msg_error "Proxmox VE가 설치되어 있지 않습니다."
fi
local pve_version=$(pveversion | head -n1 | awk '{print $2}' | cut -d'.' -f1)
if [ "$pve_version" -lt 8 ]; then
msg_error "지원되지 않는 Proxmox VE 버전입니다. 8.x 이상이 필요합니다."
fi
msg_ok "Proxmox VE $pve_version.x 버전 확인됨"
}
# 루트 권한 확인
check_root() {
if [ "$EUID" -ne 0 ]; then
msg_error "이 스크립트는 root 권한으로 실행해야 합니다. sudo를 사용하세요."
fi
}
# 입력 검증 함수
validate_rdp_server() {
local server="$1"
# 기본적인 형식 검증 (호스트:포트 또는 호스트만)
if [[ ! "$server" =~ ^[a-zA-Z0-9.-]+(:([0-9]{1,5}))?$ ]]; then
return 1
fi
# 포트 범위 검증
if [[ "$server" =~ :([0-9]+)$ ]]; then
local port="${BASH_REMATCH[1]}"
if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
return 1
fi
fi
return 0
}
validate_username() {
local username="$1"
# 사용자명 길이 및 문자 검증
if [ ${#username} -lt 1 ] || [ ${#username} -gt 32 ]; then
return 1
fi
# 허용된 문자만 포함 확인
if [[ ! "$username" =~ ^[a-zA-Z0-9._-]+$ ]]; then
return 1
fi
return 0
}
# 사용자 입력 받기
get_user_input() {
echo -e "${CYAN}RDP 연결 정보를 입력해주세요:${NC}"
echo ""
# RDP 서버 정보 (검증 포함)
while true; do
read -p "RDP 서버 주소 (예: example.com:3389): " RDP_SERVER
if [ -z "$RDP_SERVER" ]; then
msg_error "RDP 서버 주소는 필수입니다."
elif ! validate_rdp_server "$RDP_SERVER"; then
msg_warn "잘못된 서버 주소 형식입니다. 다시 입력해주세요."
echo " 예시: example.com 또는 example.com:3389"
continue
else
break
fi
done
# 사용자명 (검증 포함)
while true; do
read -p "RDP 사용자명: " RDP_USERNAME
if [ -z "$RDP_USERNAME" ]; then
msg_error "RDP 사용자명은 필수입니다."
elif ! validate_username "$RDP_USERNAME"; then
msg_warn "잘못된 사용자명 형식입니다. 영문, 숫자, ., _, - 만 사용 가능합니다."
continue
else
break
fi
done
# 패스워드 (재확인 포함)
while true; do
echo -n "RDP 패스워드: "
read -s RDP_PASSWORD
echo ""
if [ -z "$RDP_PASSWORD" ]; then
msg_error "RDP 패스워드는 필수입니다."
fi
echo -n "패스워드 확인: "
read -s password_confirm
echo ""
if [ "$RDP_PASSWORD" != "$password_confirm" ]; then
msg_warn "패스워드가 일치하지 않습니다. 다시 입력해주세요."
continue
else
break
fi
done
# 로컬 사용자명 (선택사항, 검증 포함)
while true; do
read -p "로컬 사용자명 [rdpuser]: " LOCAL_USER
LOCAL_USER=${LOCAL_USER:-rdpuser}
if ! validate_username "$LOCAL_USER"; then
msg_warn "잘못된 로컬 사용자명 형식입니다. 영문, 숫자, ., _, - 만 사용 가능합니다."
continue
else
break
fi
done
echo ""
echo -e "${YELLOW}입력된 정보:${NC}"
echo " RDP 서버: $RDP_SERVER"
echo " RDP 사용자: $RDP_USERNAME"
echo " 로컬 사용자: $LOCAL_USER"
echo ""
# 최종 확인
local attempts=0
while [ $attempts -lt 3 ]; do
read -p "설정을 계속하시겠습니까? [y/N]: " confirm
case $confirm in
[yY]|[yY][eE][sS])
return 0
;;
[nN]|[nN][oO]|"")
msg_error "설정이 취소되었습니다."
;;
*)
msg_warn "y 또는 n으로 답해주세요."
((attempts++))
;;
esac
done
msg_error "너무 많은 잘못된 입력으로 설정이 취소되었습니다."
}
# 백업 생성
create_backup() {
msg_info "기존 설정 백업 중..."
local backup_dir="/root/proxmox-rdp-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$backup_dir"
# 기존 설정 백업
if [ -f /etc/systemd/system/getty@tty1.service.d/override.conf ]; then
cp -r /etc/systemd/system/getty@tty1.service.d "$backup_dir/" 2>/dev/null || true
fi
if id "$LOCAL_USER" &>/dev/null; then
cp -r "/home/$LOCAL_USER" "$backup_dir/home-$LOCAL_USER" 2>/dev/null || true
fi
msg_ok "백업 완료: $backup_dir"
}
# 네트워크 연결 확인
check_network() {
msg_info "네트워크 연결 확인 중..."
# 인터넷 연결 확인
if ! ping -c 1 -W 5 8.8.8.8 > /dev/null 2>&1; then
msg_error "인터넷 연결을 확인할 수 없습니다. 네트워크 설정을 확인해주세요."
fi
# RDP 서버 연결 확인
local server_host="${RDP_SERVER%:*}"
local server_port="${RDP_SERVER#*:}"
# 포트가 지정되지 않았으면 기본값 3389 사용
if [ "$server_port" = "$server_host" ]; then
server_port="3389"
fi
msg_info "RDP 서버 연결 확인 중... ($server_host:$server_port)"
if command -v timeout > /dev/null; then
if ! timeout 10 bash -c "</dev/tcp/$server_host/$server_port" > /dev/null 2>&1; then
msg_warn "RDP 서버에 연결할 수 없습니다. 서버 주소와 포트를 확인해주세요."
read -p "계속 진행하시겠습니까? [y/N]: " continue_anyway
case $continue_anyway in
[yY]|[yY][eE][sS]) ;;
*) msg_error "설정이 취소되었습니다." ;;
esac
else
msg_ok "RDP 서버 연결 확인됨"
fi
else
msg_warn "연결 테스트를 건너뜁니다 (timeout 명령어 없음)"
fi
}
# 필수 패키지 설치
install_packages() {
msg_info "필수 패키지 설치 중..."
# 패키지 목록 업데이트
msg_info " - 패키지 목록 업데이트 중..."
if ! apt update > /dev/null 2>&1; then
msg_error "패키지 목록 업데이트에 실패했습니다."
fi
# 필수 패키지들
local packages="xorg openbox unclutter freerdp3-x11"
for package in $packages; do
msg_info " - $package 설치 중..."
# 패키지가 이미 설치되어 있는지 확인
if dpkg -l | grep -q "^ii $package "; then
msg_ok " $package 이미 설치됨"
continue
fi
# 패키지 설치 시도
local retry_count=0
while [ $retry_count -lt 3 ]; do
if apt install -y "$package" > /dev/null 2>&1; then
msg_ok " $package 설치 완료"
break
else
((retry_count++))
if [ $retry_count -lt 3 ]; then
msg_warn " $package 설치 실패, 재시도 중... ($retry_count/3)"
sleep 2
else
msg_error "$package 설치에 실패했습니다. 네트워크 연결이나 패키지 저장소를 확인해주세요."
fi
fi
done
done
# 설치 확인
msg_info "설치된 패키지 확인 중..."
for package in $packages; do
if ! dpkg -l | grep -q "^ii $package "; then
msg_error "$package가 정상적으로 설치되지 않았습니다."
fi
done
msg_ok "모든 패키지 설치 완료"
}
# 사용자 계정 생성
setup_user() {
msg_info "사용자 계정 설정 중..."
if ! id "$LOCAL_USER" &>/dev/null; then
useradd -m -s /bin/bash "$LOCAL_USER"
msg_ok "사용자 '$LOCAL_USER' 생성됨"
else
msg_warn "사용자 '$LOCAL_USER'가 이미 존재합니다."
fi
# 홈 디렉토리 권한 설정
chown -R "$LOCAL_USER:$LOCAL_USER" "/home/$LOCAL_USER"
}
# 자동 로그인 설정
setup_autologin() {
msg_info "자동 로그인 설정 중..."
# 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 $LOCAL_USER --noclear %I \$TERM
Type=idle
EOF
# systemd 리로드
systemctl daemon-reload
msg_ok "자동 로그인 설정 완료"
}
# X 자동 시작 설정
setup_x_autostart() {
msg_info "X Window 자동 시작 설정 중..."
# .bash_profile 생성
cat > "/home/$LOCAL_USER/.bash_profile" << 'EOF'
# tty1에서만 X 자동 시작
if [[ -z $DISPLAY ]] && [[ $(tty) == /dev/tty1 ]]; then
startx
logout
fi
EOF
chown "$LOCAL_USER:$LOCAL_USER" "/home/$LOCAL_USER/.bash_profile"
msg_ok "X Window 자동 시작 설정 완료"
}
# RDP 연결 설정
setup_rdp_connection() {
msg_info "RDP 연결 설정 중..."
# .xinitrc 파일 생성
cat > "/home/$LOCAL_USER/.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:$RDP_SERVER \\
/u:$RDP_USERNAME \\
/p:"$RDP_PASSWORD" \\
+f \\
/cert:ignore \\
+dynamic-resolution \\
/sound:sys:alsa \\
+clipboard
# RDP 종료 시 X 세션도 종료
pkill -SIGTERM Xorg
EOF
# 실행 권한 및 소유권 설정
chmod +x "/home/$LOCAL_USER/.xinitrc"
chown "$LOCAL_USER:$LOCAL_USER" "/home/$LOCAL_USER/.xinitrc"
msg_ok "RDP 연결 설정 완료"
}
# Openbox 설정
setup_openbox() {
msg_info "Openbox 윈도우 매니저 설정 중..."
# Openbox 설정 디렉토리 생성
mkdir -p "/home/$LOCAL_USER/.config/openbox"
# rc.xml 설정 파일 생성
cat > "/home/$LOCAL_USER/.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 "$LOCAL_USER:$LOCAL_USER" "/home/$LOCAL_USER/.config"
msg_ok "Openbox 설정 완료"
}
# 설정 테스트
test_configuration() {
msg_info "설정 테스트 중..."
# systemd 서비스 상태 확인
if systemctl is-enabled getty@tty1.service > /dev/null 2>&1; then
msg_ok "getty@tty1 서비스가 활성화되어 있습니다."
else
msg_warn "getty@tty1 서비스가 비활성화되어 있습니다."
fi
# 필수 파일들 존재 확인
local files=(
"/etc/systemd/system/getty@tty1.service.d/override.conf"
"/home/$LOCAL_USER/.bash_profile"
"/home/$LOCAL_USER/.xinitrc"
"/home/$LOCAL_USER/.config/openbox/rc.xml"
)
for file in "${files[@]}"; do
if [ -f "$file" ]; then
msg_ok "설정 파일 존재: $file"
else
msg_error "설정 파일 누락: $file"
fi
done
# FreeRDP3 설치 확인
if command -v xfreerdp3 > /dev/null 2>&1; then
msg_ok "FreeRDP3 설치 확인됨"
else
msg_error "FreeRDP3가 설치되어 있지 않습니다."
fi
}
# 완료 메시지
print_completion() {
echo ""
echo -e "${GREEN}═══════════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} 설정이 완료되었습니다!${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e "${CYAN}설정 요약:${NC}"
echo " - RDP 서버: $RDP_SERVER"
echo " - RDP 사용자: $RDP_USERNAME"
echo " - 로컬 사용자: $LOCAL_USER"
echo " - 자동 로그인: 활성화됨 (tty1)"
echo " - 풀스크린 RDP: 활성화됨"
echo ""
echo -e "${YELLOW}다음 단계:${NC}"
echo " 1. 시스템을 재부팅하세요"
echo " 2. 자동으로 RDP 연결이 시작됩니다"
echo " 3. 문제 발생 시 Ctrl+Alt+F2로 다른 터미널에 접근 가능합니다"
echo ""
echo -e "${CYAN}재부팅하시겠습니까? [y/N]:${NC} "
read -r reboot_confirm
case $reboot_confirm in
[yY]|[yY][eE][sS])
msg_info "시스템을 재부팅합니다..."
sleep 2
reboot
;;
*)
echo -e "${GREEN}설정이 완료되었습니다. 수동으로 재부팅해 주세요.${NC}"
;;
esac
}
# 메인 함수
main() {
print_header
# 사전 검사
check_root
check_proxmox_version
# 사용자 입력
get_user_input
# 네트워크 확인
check_network
# 백업 생성
create_backup
# 설정 수행
install_packages
setup_user
setup_autologin
setup_x_autostart
setup_rdp_connection
setup_openbox
# 테스트 및 완료
test_configuration
print_completion
}
# 에러 핸들링
trap 'msg_error "스크립트 실행 중 오류가 발생했습니다."' ERR
# 스크립트 실행
main "$@"

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