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:
parent
1f926d6b35
commit
c6919abf1c
160
RDP/RDP_TOGGLE_API.md
Normal file
160
RDP/RDP_TOGGLE_API.md
Normal 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
95
RDP/README.md
Normal 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
62
RDP/install-rdp-api.sh
Executable 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\"}'"
|
||||
212
RDP/proxmox_auto_rdp_setup_korean.md
Normal file
212
RDP/proxmox_auto_rdp_setup_korean.md
Normal 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
276
RDP/rdp-toggle-api.py
Normal 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
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>
|
||||
4
RDP/requirements.txt
Normal file
4
RDP/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
fastapi==0.115.5
|
||||
uvicorn==0.32.1
|
||||
python-multipart==0.0.20
|
||||
pydantic==2.10.3
|
||||
Loading…
Reference in New Issue
Block a user