Spaces:
Sleeping
Sleeping
이정수/품질관리팀
commited on
Commit
·
b7ede89
1
Parent(s):
79e1e8d
fix
Browse files
app.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import requests
|
| 3 |
from requests.exceptions import RequestException
|
| 4 |
import json
|
| 5 |
import time
|
| 6 |
|
| 7 |
-
# 전역
|
| 8 |
SERVER_URL = ""
|
| 9 |
-
# 타임아웃 설정 (초)
|
| 10 |
REQUEST_TIMEOUT = 15
|
| 11 |
-
|
| 12 |
-
# 영구적인 HTTP 세션 생성 및 재시도 설정 (이전과 동일)
|
| 13 |
session = requests.Session()
|
| 14 |
adapter = requests.adapters.HTTPAdapter(
|
| 15 |
-
pool_connections=5,
|
| 16 |
-
pool_maxsize=10,
|
| 17 |
max_retries=requests.adapters.Retry(
|
| 18 |
total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504],
|
| 19 |
allowed_methods=["GET", "POST"], connect=3, read=3, redirect=3
|
|
@@ -22,258 +22,352 @@ adapter = requests.adapters.HTTPAdapter(
|
|
| 22 |
session.mount('http://', adapter)
|
| 23 |
session.mount('https://', adapter)
|
| 24 |
|
|
|
|
|
|
|
| 25 |
def connect_server(ngrok_url):
|
| 26 |
-
"""서버 연결 시도"""
|
| 27 |
global SERVER_URL
|
|
|
|
| 28 |
try:
|
| 29 |
-
if not ngrok_url:
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
if
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
def check_status():
|
| 41 |
-
"""
|
| 42 |
global SERVER_URL
|
| 43 |
-
if not SERVER_URL:
|
|
|
|
|
|
|
| 44 |
try:
|
| 45 |
response = session.get(f"{SERVER_URL}/api/status", timeout=REQUEST_TIMEOUT)
|
| 46 |
if response.status_code == 200:
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
else:
|
| 49 |
-
|
|
|
|
| 50 |
except RequestException as e:
|
| 51 |
-
|
|
|
|
| 52 |
|
| 53 |
def echo_test(message):
|
| 54 |
-
"""
|
| 55 |
global SERVER_URL
|
| 56 |
-
if not SERVER_URL:
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
try:
|
| 59 |
payload = {"action": "echo", "data": {"message": message}, "timestamp": int(time.time() * 1000)}
|
| 60 |
response = session.post(f"{SERVER_URL}/api/send", json=payload, headers={"Content-Type": "application/json"}, timeout=REQUEST_TIMEOUT)
|
| 61 |
if response.status_code == 200:
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
else:
|
| 64 |
-
|
|
|
|
| 65 |
except RequestException as e:
|
| 66 |
-
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
# --- get_programs 함수 수정 ---
|
| 69 |
def get_programs():
|
| 70 |
-
"""프로그램 목록 조회
|
| 71 |
global SERVER_URL
|
| 72 |
print(f"[DEBUG] 프로그램 목록 조회 함수 시작")
|
| 73 |
-
program_options = []
|
|
|
|
| 74 |
|
| 75 |
-
# 서버 URL
|
| 76 |
if not SERVER_URL:
|
| 77 |
-
print(f"[DEBUG]
|
| 78 |
test_programs = [
|
| 79 |
-
{"id": "test1", "name": "테스트
|
| 80 |
-
{"id": "test2", "name": "테스트
|
| 81 |
]
|
| 82 |
-
#
|
| 83 |
-
html_output
|
| 84 |
-
html_output += "<
|
| 85 |
-
html_output += "<
|
| 86 |
-
html_output += "<
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
html_output += "</table></div>"
|
| 96 |
-
print(f"[DEBUG] 테스트 프로그램 옵션: {program_options}")
|
| 97 |
-
# HTML 결과와 함께 드롭다운 업데이트 객체 반환
|
| 98 |
-
return html_output, gr.Dropdown.update(choices=program_options), gr.Dropdown.update(choices=program_options)
|
| 99 |
-
|
| 100 |
-
# 서버 URL이 있는 경우 API 호출
|
| 101 |
-
print(f"[DEBUG] API 호출 시도: {SERVER_URL}/api/programs")
|
| 102 |
try:
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
| 106 |
|
| 107 |
if response.status_code == 200:
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
else:
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
return error_html, gr.
|
| 137 |
except RequestException as e:
|
| 138 |
-
print(f"[
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
]
|
| 146 |
-
for program in test_programs:
|
| 147 |
-
program_options.append((f"{program['name']} ({program['description']})", program['id']))
|
| 148 |
|
| 149 |
-
error_html = f"<div style='padding: 15px; background-color: #f8d7da; border-radius: 5px; border-left: 5px solid #dc3545;'><h3 style='margin-top: 0; color: #721c24;'>프로그램 목록 조회 중 오류 발생</h3><p>{str(e)}</p><p><strong>오류 유형:</strong> {error_type}</p><p style='margin-top: 15px;'><strong>테스트 모드:</strong> 임시 테스트용 데이터가 드롭다운에 로드되었습니다.</p></div>"
|
| 150 |
-
print(f"[DEBUG] 오류 시 테스트 옵션: {program_options}")
|
| 151 |
-
# HTML 오류 메시지와 함께 테스트용 드롭다운 업데이트 객체 반환
|
| 152 |
-
return error_html, gr.Dropdown.update(choices=program_options), gr.Dropdown.update(choices=program_options)
|
| 153 |
|
| 154 |
-
# --- execute_program 함수는 이전과 거의 동일 ---
|
| 155 |
def execute_program(program_id):
|
| 156 |
-
"""프로그램 실행"""
|
| 157 |
global SERVER_URL
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
-
# 테스트
|
| 162 |
-
if program_id.startswith("test")
|
| 163 |
-
print(f"[DEBUG] 테스트 프로그램
|
| 164 |
-
|
| 165 |
-
return html_output
|
| 166 |
|
| 167 |
if not SERVER_URL:
|
| 168 |
-
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
|
| 171 |
try:
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
if response.status_code == 200:
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
| 182 |
else:
|
| 183 |
-
|
|
|
|
|
|
|
| 184 |
except RequestException as e:
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
-
# --- UI 구성 수정 ---
|
| 196 |
-
with gr.Blocks(title="LocalPCAgent 제어 인터페이스") as demo:
|
| 197 |
gr.Markdown("# LocalPCAgent 제어 인터페이스")
|
| 198 |
-
gr.Markdown("
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
with gr.Row():
|
| 203 |
-
ngrok_url_input = gr.Textbox(
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
connection_status = gr.Textbox(label="연결 상태", interactive=False)
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
with gr.Row():
|
| 223 |
-
get_programs_btn = gr.Button("프로그램 목록 가져오기")
|
| 224 |
-
gr.Markdown(f"<p style='color:#666; font-size:0.9em;'>이 작업은 최대 {REQUEST_TIMEOUT}초가 소요될 수 있습니다.</p>")
|
| 225 |
-
# HTML 결과 표시 영역
|
| 226 |
-
programs_result = gr.HTML(label="프로그램 목록 결과")
|
| 227 |
-
# 임시 드롭다운 (선택지 표시용, UI상에선 크게 의미 없음) - 업데이트 대상 O
|
| 228 |
-
program_dropdown_display = gr.Dropdown(label="조회된 프로그램 (참고용)", choices=[], interactive=False)
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
-
with gr.
|
| 232 |
-
gr.Markdown("
|
| 233 |
-
# 실제 사용자가 선택할 드롭다운
|
| 234 |
-
program_dropdown_exec = gr.Dropdown(
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
# 실행 결과 표시 영역
|
| 237 |
execute_result = gr.HTML(label="실행 결과")
|
| 238 |
-
gr.Markdown("<small>* 위에서 '프로그램 목록 가져오기'를 먼저 실행한 후, 여기서 프로그램을 선택하고 실행하세요.</small>")
|
| 239 |
|
| 240 |
|
| 241 |
-
# --- 이벤트 핸들러
|
| 242 |
-
connect_btn.click(fn=connect_server, inputs=
|
| 243 |
-
check_status_btn.click(fn=check_status, inputs=None, outputs=
|
| 244 |
-
echo_btn.click(fn=echo_test, inputs=
|
| 245 |
|
| 246 |
-
#
|
| 247 |
-
# get_programs 함수는 (html_output, dropdown_update1, dropdown_update2)를 반환
|
| 248 |
-
# outputs 리스트의 순서에 맞게 각 컴포넌트가 업데이트됨
|
| 249 |
get_programs_btn.click(
|
| 250 |
fn=get_programs,
|
| 251 |
inputs=None,
|
| 252 |
-
outputs
|
|
|
|
| 253 |
)
|
| 254 |
|
| 255 |
-
# 실행할 프로그램 드롭다운
|
| 256 |
-
program_dropdown_exec.change(
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
),
|
| 262 |
-
inputs=[program_dropdown_exec],
|
| 263 |
-
outputs=[execute_result] # 실행 결과 영역에 선택 상태 표시
|
| 264 |
-
)
|
| 265 |
|
| 266 |
-
#
|
| 267 |
execute_program_btn.click(
|
| 268 |
fn=execute_program,
|
| 269 |
-
inputs=[program_dropdown_exec], #
|
| 270 |
-
outputs=[execute_result] # 실행
|
| 271 |
)
|
| 272 |
|
| 273 |
-
# 앱 실행
|
| 274 |
if __name__ == "__main__":
|
| 275 |
-
print("
|
| 276 |
-
print("
|
| 277 |
-
print("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
demo.launch(debug=True, show_error=True)
|
| 279 |
-
print("
|
|
|
|
|
|
|
|
|
| 1 |
+
# requirements.txt 에 다음 내용을 추가하거나 업데이트해야 합니다:
|
| 2 |
+
# gradio>=4.0
|
| 3 |
+
# requests
|
| 4 |
+
|
| 5 |
import gradio as gr
|
| 6 |
import requests
|
| 7 |
from requests.exceptions import RequestException
|
| 8 |
import json
|
| 9 |
import time
|
| 10 |
|
| 11 |
+
# --- 전역 변수 및 세션 설정 ---
|
| 12 |
SERVER_URL = ""
|
|
|
|
| 13 |
REQUEST_TIMEOUT = 15
|
|
|
|
|
|
|
| 14 |
session = requests.Session()
|
| 15 |
adapter = requests.adapters.HTTPAdapter(
|
| 16 |
+
pool_connections=5, pool_maxsize=10,
|
|
|
|
| 17 |
max_retries=requests.adapters.Retry(
|
| 18 |
total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504],
|
| 19 |
allowed_methods=["GET", "POST"], connect=3, read=3, redirect=3
|
|
|
|
| 22 |
session.mount('http://', adapter)
|
| 23 |
session.mount('https://', adapter)
|
| 24 |
|
| 25 |
+
# --- 백엔드 통신 함수 ---
|
| 26 |
+
|
| 27 |
def connect_server(ngrok_url):
|
| 28 |
+
"""입력된 Ngrok URL로 서버 연결 시도"""
|
| 29 |
global SERVER_URL
|
| 30 |
+
print(f"[DEBUG] 서버 연결 시도: {ngrok_url}")
|
| 31 |
try:
|
| 32 |
+
if not ngrok_url:
|
| 33 |
+
return "오류: 서버 URL을 입력해주세요."
|
| 34 |
+
# URL 끝 슬래시 제거
|
| 35 |
+
if ngrok_url.endswith('/'):
|
| 36 |
+
ngrok_url = ngrok_url[:-1]
|
| 37 |
+
|
| 38 |
+
# '/health' 또는 '/api/status' 엔드포인트로 상태 확인
|
| 39 |
+
health_url = f"{ngrok_url}/health"
|
| 40 |
+
status_url = f"{ngrok_url}/api/status"
|
| 41 |
+
test_url = ""
|
| 42 |
+
response = None
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
test_url = health_url
|
| 46 |
+
print(f"[DEBUG] {test_url} 확인 중...")
|
| 47 |
+
response = session.get(test_url, timeout=5) # 상태 확인은 짧은 타임아웃
|
| 48 |
+
if response.status_code != 200:
|
| 49 |
+
raise RequestException("Health check failed or returned non-200")
|
| 50 |
+
except RequestException as e_health:
|
| 51 |
+
print(f"[DEBUG] {test_url} 실패 ({e_health}), {status_url} 로 재시도...")
|
| 52 |
+
try:
|
| 53 |
+
test_url = status_url
|
| 54 |
+
response = session.get(test_url, timeout=5) # 상태 확인은 짧은 타임아웃
|
| 55 |
+
if response.status_code != 200:
|
| 56 |
+
raise RequestException("Status check failed or returned non-200")
|
| 57 |
+
except RequestException as e_status:
|
| 58 |
+
print(f"[ERROR] 서버 연결 최종 실패: {e_status}")
|
| 59 |
+
return f"서버 연결 실패: {test_url} 확인 중 오류 발생 ({e_status})"
|
| 60 |
+
|
| 61 |
+
# 연결 성공 시
|
| 62 |
+
SERVER_URL = ngrok_url
|
| 63 |
+
print(f"[INFO] 서버 연결 성공: {SERVER_URL}")
|
| 64 |
+
return f"서버 연결 성공! ({test_url} 응답코드: {response.status_code})"
|
| 65 |
+
|
| 66 |
+
except Exception as e: # 포괄적인 예외 처리
|
| 67 |
+
print(f"[ERROR] 서버 연결 중 예외 발생: {e}")
|
| 68 |
+
return f"서버 연결 중 예상치 못한 오류 발생: {str(e)}"
|
| 69 |
+
|
| 70 |
|
| 71 |
def check_status():
|
| 72 |
+
"""서버의 /api/status 엔드포인트 상태 확인"""
|
| 73 |
global SERVER_URL
|
| 74 |
+
if not SERVER_URL:
|
| 75 |
+
return gr.update(value="오류: 서버에 먼저 연결하세요.")
|
| 76 |
+
print(f"[DEBUG] 서버 상태 확인 요청: {SERVER_URL}/api/status")
|
| 77 |
try:
|
| 78 |
response = session.get(f"{SERVER_URL}/api/status", timeout=REQUEST_TIMEOUT)
|
| 79 |
if response.status_code == 200:
|
| 80 |
+
try:
|
| 81 |
+
status_data = json.dumps(response.json(), indent=2, ensure_ascii=False)
|
| 82 |
+
print(f"[INFO] 상태 확인 성공")
|
| 83 |
+
return gr.update(value=status_data)
|
| 84 |
+
except json.JSONDecodeError:
|
| 85 |
+
print(f"[ERROR] 상태 응답 JSON 파싱 실패")
|
| 86 |
+
return gr.update(value="오류: 서버 응답 형식이 잘못되었습니다 (JSON 아님).")
|
| 87 |
else:
|
| 88 |
+
print(f"[ERROR] 상태 확인 실패: HTTP {response.status_code}")
|
| 89 |
+
return gr.update(value=f"상태 확인 실패: HTTP {response.status_code}\n{response.text}")
|
| 90 |
except RequestException as e:
|
| 91 |
+
print(f"[ERROR] 상태 확인 중 네트워크 오류: {e}")
|
| 92 |
+
return gr.update(value=f"상태 확인 중 오류 발생: {str(e)}")
|
| 93 |
|
| 94 |
def echo_test(message):
|
| 95 |
+
"""서버의 /api/send (echo 액션) 테스트"""
|
| 96 |
global SERVER_URL
|
| 97 |
+
if not SERVER_URL:
|
| 98 |
+
return gr.update(value="오류: 서버에 먼저 연결하세요.")
|
| 99 |
+
if not message:
|
| 100 |
+
return gr.update(value="오류: 에코 테스트 메시지를 입력하세요.")
|
| 101 |
+
print(f"[DEBUG] 에코 테스트 요청: {SERVER_URL}/api/send, 메시지: {message}")
|
| 102 |
try:
|
| 103 |
payload = {"action": "echo", "data": {"message": message}, "timestamp": int(time.time() * 1000)}
|
| 104 |
response = session.post(f"{SERVER_URL}/api/send", json=payload, headers={"Content-Type": "application/json"}, timeout=REQUEST_TIMEOUT)
|
| 105 |
if response.status_code == 200:
|
| 106 |
+
try:
|
| 107 |
+
echo_data = json.dumps(response.json(), indent=2, ensure_ascii=False)
|
| 108 |
+
print(f"[INFO] 에코 테스트 성공")
|
| 109 |
+
return gr.update(value=echo_data)
|
| 110 |
+
except json.JSONDecodeError:
|
| 111 |
+
print(f"[ERROR] 에코 응답 JSON 파싱 실패")
|
| 112 |
+
return gr.update(value="오류: 서버 응답 형식이 잘못되었습니다 (JSON 아님).")
|
| 113 |
else:
|
| 114 |
+
print(f"[ERROR] 에코 테스트 실패: HTTP {response.status_code}")
|
| 115 |
+
return gr.update(value=f"에코 테스트 실패: HTTP {response.status_code}\n{response.text}")
|
| 116 |
except RequestException as e:
|
| 117 |
+
print(f"[ERROR] 에코 테스트 중 네트워크 오류: {e}")
|
| 118 |
+
return gr.update(value=f"에코 테스트 중 오류 발생: {str(e)}")
|
| 119 |
+
|
| 120 |
|
|
|
|
| 121 |
def get_programs():
|
| 122 |
+
"""서버의 /api/programs 에서 프로그램 목록 조회"""
|
| 123 |
global SERVER_URL
|
| 124 |
print(f"[DEBUG] 프로그램 목록 조회 함수 시작")
|
| 125 |
+
program_options = []
|
| 126 |
+
default_return = ("<div style='color: gray;'>프로그램 목록이 없습니다.</div>", gr.update(choices=[]), gr.update(choices=[]))
|
| 127 |
|
| 128 |
+
# 테스트 모드 (서버 URL 없을 때)
|
| 129 |
if not SERVER_URL:
|
| 130 |
+
print(f"[DEBUG] 테스트 모드: 프로그램 목록 생성")
|
| 131 |
test_programs = [
|
| 132 |
+
{"id": "test1", "name": "테스트 메모장", "description": "텍스트 편집기", "path": "C:\\Windows\\notepad.exe"},
|
| 133 |
+
{"id": "test2", "name": "테스트 계산기", "description": "기본 계산기", "path": "C:\\Windows\\System32\\calc.exe"}
|
| 134 |
]
|
| 135 |
+
html_output = "<div style='padding: 10px; background-color: #fffaeb; border: 1px solid #ffecb3; border-radius: 5px;'>"
|
| 136 |
+
html_output += "<h4 style='margin-top:0; color: #856404;'>[테스트 모드]</h4>"
|
| 137 |
+
html_output += "<p style='font-size: 0.9em;'>서버에 연결되지 않아 테스트 데이터가 표시됩니다.</p>"
|
| 138 |
+
html_output += "<table style='width: 100%; border-collapse: collapse; margin-top: 10px;'><thead><tr style='background-color: #fff3cd;'>"
|
| 139 |
+
html_output += "<th style='padding: 8px; text-align: left;'>이름</th><th style='padding: 8px; text-align: left;'>설명</th></tr></thead><tbody>"
|
| 140 |
+
for p in test_programs:
|
| 141 |
+
program_options.append((f"{p['name']} ({p['description']})", p['id']))
|
| 142 |
+
html_output += f"<tr style='border-bottom: 1px solid #eee;'><td style='padding: 8px;'>{p['name']}</td><td style='padding: 8px;'>{p['description']}</td></tr>"
|
| 143 |
+
html_output += "</tbody></table></div>"
|
| 144 |
+
print(f"[DEBUG] 테스트 옵션: {program_options}")
|
| 145 |
+
return html_output, gr.update(choices=program_options), gr.update(choices=program_options)
|
| 146 |
+
|
| 147 |
+
# 실제 API 호출 (서버 URL 있을 때)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
try:
|
| 149 |
+
api_url = f"{SERVER_URL}/api/programs"
|
| 150 |
+
print(f"[DEBUG] API 호출: {api_url}")
|
| 151 |
+
headers = {"User-Agent": "GradioClient/1.0", "Accept": "application/json"}
|
| 152 |
+
response = session.get(api_url, headers=headers, timeout=REQUEST_TIMEOUT)
|
| 153 |
+
print(f"[DEBUG] API 응답 코드: {response.status_code}")
|
| 154 |
|
| 155 |
if response.status_code == 200:
|
| 156 |
+
try:
|
| 157 |
+
result_json = response.json()
|
| 158 |
+
programs = result_json.get("programs", [])
|
| 159 |
+
if not programs: # 프로그램 목록이 비어있을 경우
|
| 160 |
+
print("[INFO] 서버에서 반환된 프로그램 목록이 비어있습니다.")
|
| 161 |
+
return "<div style='color: gray; padding: 10px;'>서버에 등록된 프로그램이 없습니다.</div>", gr.update(choices=[]), gr.update(choices=[])
|
| 162 |
+
|
| 163 |
+
# 프로그램 목록 테이블 생성
|
| 164 |
+
html_output = "<div style='max-height: 300px; overflow-y: auto; border: 1px solid #eee; padding: 10px; border-radius: 5px;'>"
|
| 165 |
+
html_output += "<table style='width: 100%; border-collapse: collapse;'><thead><tr style='background-color: #f8f9fa;'>"
|
| 166 |
+
html_output += "<th style='padding: 8px; text-align: left;'>이름</th><th style='padding: 8px; text-align: left;'>설명</th><th style='padding: 8px; text-align: left;'>경로</th></tr></thead><tbody>"
|
| 167 |
+
for program in programs:
|
| 168 |
+
program_id = program.get("id", "")
|
| 169 |
+
program_name = program.get("name", "N/A")
|
| 170 |
+
program_desc = program.get("description", "")
|
| 171 |
+
program_path = program.get("path", "")
|
| 172 |
+
# ID가 유효한 경우에만 옵션 추가
|
| 173 |
+
if program_id:
|
| 174 |
+
program_options.append((f"{program_name} ({program_desc})", program_id))
|
| 175 |
+
html_output += f"<tr style='border-bottom: 1px solid #eee;'><td style='padding: 8px;'>{program_name}</td><td style='padding: 8px;'>{program_desc}</td><td style='padding: 8px;'>{program_path}</td></tr>"
|
| 176 |
+
else:
|
| 177 |
+
print(f"[WARN] ID가 없는 프로그램 데이터 발견: {program}")
|
| 178 |
+
html_output += f"<tr style='border-bottom: 1px solid #eee; background-color: #fff3cd;'><td style='padding: 8px; color: orange;' colspan='3'>[주의] ID가 없는 프로그램: {program_name}</td></tr>"
|
| 179 |
+
|
| 180 |
+
html_output += "</tbody></table></div>"
|
| 181 |
+
html_output += f"<div style='margin-top: 10px; font-size: 0.9em; color: #666;'>총 {len(program_options)}개 프로그램 (ID 유효 기준)</div>"
|
| 182 |
+
print(f"[INFO] 프로그램 목록 조회 성공, 옵션 개수: {len(program_options)}")
|
| 183 |
+
return html_output, gr.update(choices=program_options), gr.update(choices=program_options)
|
| 184 |
+
|
| 185 |
+
except json.JSONDecodeError:
|
| 186 |
+
print(f"[ERROR] 프로그램 목록 응답 JSON 파싱 실패")
|
| 187 |
+
error_html = "<div style='color: red;'>오류: 서버 응답 형식이 잘못되었습니다 (JSON 아님).</div>"
|
| 188 |
+
return error_html, gr.update(choices=[]), gr.update(choices=[])
|
| 189 |
else:
|
| 190 |
+
print(f"[ERROR] 프로그램 목록 조회 실패: HTTP {response.status_code}")
|
| 191 |
+
error_html = f"<div style='color: red;'>프로그램 목록 조회 실패: HTTP {response.status_code}<br>{response.text}</div>"
|
| 192 |
+
return error_html, gr.update(choices=[]), gr.update(choices=[])
|
| 193 |
except RequestException as e:
|
| 194 |
+
print(f"[ERROR] 프로그램 목록 조회 중 네트워크 오류: {e}")
|
| 195 |
+
error_html = f"<div style='color: red;'>프로그램 목록 조회 중 오류 발생: {str(e)}</div>"
|
| 196 |
+
return error_html, gr.update(choices=[]), gr.update(choices=[])
|
| 197 |
+
except Exception as e: # 포괄적 예외 처리
|
| 198 |
+
print(f"[ERROR] 프로그램 목록 조회 중 예상치 못한 오류: {e}")
|
| 199 |
+
error_html = f"<div style='color: red;'>프로그램 목록 조회 중 예상치 못한 오류: {str(e)}</div>"
|
| 200 |
+
return error_html, gr.update(choices=[]), gr.update(choices=[])
|
|
|
|
|
|
|
|
|
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
|
|
|
| 203 |
def execute_program(program_id):
|
| 204 |
+
"""선택된 프로그램 ID를 사용하여 서버에 실행 요청"""
|
| 205 |
global SERVER_URL
|
| 206 |
+
# 입력 값 타입 로깅 및 검증
|
| 207 |
+
print(f"[DEBUG] 프로그램 실행 시도: ID='{program_id}', Type={type(program_id)}")
|
| 208 |
+
if not isinstance(program_id, str):
|
| 209 |
+
error_msg = f"오류: 프로그램 ID 형식이 잘못되었습니다 (받은 타입: {type(program_id)}). 프로그램을 다시 선택해주세요."
|
| 210 |
+
print(f"[ERROR] {error_msg}")
|
| 211 |
+
return f"<div style='color: red;' class='error-box'>{error_msg}</div>"
|
| 212 |
+
if not program_id:
|
| 213 |
+
error_msg = "오류: 실행할 프로그램을 선택해주세요."
|
| 214 |
+
print(f"[WARN] {error_msg}")
|
| 215 |
+
return f"<div style='color: orange;' class='warning-box'>{error_msg}</div>"
|
| 216 |
|
| 217 |
+
# 테스트 ID 처리
|
| 218 |
+
if program_id.startswith("test"):
|
| 219 |
+
print(f"[DEBUG] 테스트 프로그램 '{program_id}' 실행 시뮬레이션")
|
| 220 |
+
return f"<div style='color: green;' class='success-box'>테스트 프로그램 '{program_id}' 실행 완료 (시뮬레이션)</div>"
|
|
|
|
| 221 |
|
| 222 |
if not SERVER_URL:
|
| 223 |
+
error_msg = "오류: 서버에 먼저 연결해야 합니다."
|
| 224 |
+
print(f"[WARN] {error_msg}")
|
| 225 |
+
return f"<div style='color: orange;' class='warning-box'>{error_msg}</div>"
|
| 226 |
|
| 227 |
+
# 실제 API 호출
|
| 228 |
try:
|
| 229 |
+
api_url = f"{SERVER_URL}/api/programs/{program_id}/execute"
|
| 230 |
+
print(f"[DEBUG] 서버 실행 요청: {api_url}")
|
| 231 |
+
headers = {"User-Agent": "GradioClient/1.0", "Accept": "application/json", "Content-Type": "application/json"}
|
| 232 |
+
# POST 요청의 본문에 ID 포함 (서버 API 구현에 따라 필요 없을 수도 있음)
|
| 233 |
+
payload = {"id": program_id}
|
| 234 |
+
response = session.post(api_url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT)
|
| 235 |
+
print(f"[DEBUG] 실행 API 응답 코드: {response.status_code}")
|
| 236 |
|
| 237 |
if response.status_code == 200:
|
| 238 |
+
try:
|
| 239 |
+
result = response.json()
|
| 240 |
+
msg = result.get('message', '프로그램이 성공적으로 실행되었습니다.')
|
| 241 |
+
print(f"[INFO] 프로그램 실행 성공: {msg}")
|
| 242 |
+
return f"<div style='color: green;' class='success-box'>실행 성공: {msg}</div>"
|
| 243 |
+
except json.JSONDecodeError:
|
| 244 |
+
print(f"[ERROR] 실행 응답 JSON 파싱 실패")
|
| 245 |
+
return f"<div style='color: red;' class='error-box'>오류: 실행 응답 형식이 잘못되었습니다 (JSON 아님).</div>"
|
| 246 |
else:
|
| 247 |
+
error_text = response.text # 오류 메시지 포함
|
| 248 |
+
print(f"[ERROR] 프로그램 실행 실패: HTTP {response.status_code}, 응답: {error_text}")
|
| 249 |
+
return f"<div style='color: red;' class='error-box'>실행 실패: HTTP {response.status_code}<br><pre>{error_text}</pre></div>"
|
| 250 |
except RequestException as e:
|
| 251 |
+
error_msg = f"실행 중 네트워크 오류 발생: {str(e)}"
|
| 252 |
+
print(f"[ERROR] {error_msg}")
|
| 253 |
+
return f"<div style='color: red;' class='error-box'>{error_msg}</div>"
|
| 254 |
+
except Exception as e: # 포괄적 예외 처리
|
| 255 |
+
error_msg = f"실행 중 예상치 못한 오류 발생: {str(e)}"
|
| 256 |
+
print(f"[ERROR] {error_msg}")
|
| 257 |
+
return f"<div style='color: red;' class='error-box'>{error_msg}</div>"
|
| 258 |
+
|
| 259 |
|
| 260 |
+
# --- Gradio UI 구성 (최신 버전 스타일) ---
|
| 261 |
+
with gr.Blocks(title="LocalPCAgent 제어 인터페이스 v4", theme=gr.themes.Soft()) as demo:
|
| 262 |
+
# 사용자 정의 CSS (선택 사항)
|
| 263 |
+
gr.HTML("""
|
| 264 |
+
<style>
|
| 265 |
+
.error-box { padding: 10px; border: 1px solid #dc3545; border-radius: 5px; background-color: #f8d7da; }
|
| 266 |
+
.success-box { padding: 10px; border: 1px solid #28a745; border-radius: 5px; background-color: #d4edda; }
|
| 267 |
+
.warning-box { padding: 10px; border: 1px solid #ffc107; border-radius: 5px; background-color: #fff3cd; }
|
| 268 |
+
table { font-size: 0.9em; }
|
| 269 |
+
th, td { padding: 6px 8px; border-bottom: 1px solid #eee; }
|
| 270 |
+
th { background-color: #f8f9fa; font-weight: 500; }
|
| 271 |
+
tbody tr:hover { background-color: #f1f1f1; }
|
| 272 |
+
</style>
|
| 273 |
+
""")
|
| 274 |
|
|
|
|
|
|
|
| 275 |
gr.Markdown("# LocalPCAgent 제어 인터페이스")
|
| 276 |
+
gr.Markdown("원격 PC의 상태 확인 및 프로그램 실행을 위한 웹 UI입니다.")
|
| 277 |
|
| 278 |
+
# 섹션 1: 서버 연결
|
| 279 |
+
with gr.Accordion("1. 서버 연결 설정", open=True): # Accordion 사용
|
| 280 |
with gr.Row():
|
| 281 |
+
ngrok_url_input = gr.Textbox(
|
| 282 |
+
label="Ngrok URL 입력",
|
| 283 |
+
placeholder="https://xxxx-xx-xxx-xxx.ngrok-free.app 형식",
|
| 284 |
+
scale=3 # 너비 비율
|
| 285 |
+
)
|
| 286 |
+
connect_btn = gr.Button("서버 연결", variant="secondary", scale=1)
|
| 287 |
connection_status = gr.Textbox(label="연결 상태", interactive=False)
|
| 288 |
|
| 289 |
+
# 섹션 2: 기능 탭
|
| 290 |
+
with gr.Tabs():
|
| 291 |
+
# 탭 1: 기본 기능
|
| 292 |
+
with gr.Tab("기본 기능"):
|
| 293 |
+
gr.Markdown("서버의 기본 상태를 확인하고 통신 테스트를 수행합니다.")
|
| 294 |
+
with gr.Row(equal_height=True): # Row 안의 요소들 높이 맞춤
|
| 295 |
+
with gr.Column(scale=1):
|
| 296 |
+
gr.Markdown("#### 서버 상태")
|
| 297 |
+
check_status_btn = gr.Button("상태 확인", variant="secondary")
|
| 298 |
+
gr.Markdown("#### 에코 테스트")
|
| 299 |
+
echo_message = gr.Textbox(label="보낼 메시지", value="Hello from Gradio!", lines=1)
|
| 300 |
+
echo_btn = gr.Button("에코 테스트 전송", variant="secondary")
|
| 301 |
+
with gr.Column(scale=2):
|
| 302 |
+
status_result = gr.Textbox(label="상태 결과", interactive=False, lines=5, max_lines=10)
|
| 303 |
+
echo_result = gr.Textbox(label="에코 결과", interactive=False, lines=5, max_lines=10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
+
# 탭 2: 프로그램 실행
|
| 306 |
+
with gr.Tab("프로그램 실행"):
|
| 307 |
+
gr.Markdown("서버에 등록된 프로그램을 조회하고 실행합니다.")
|
| 308 |
+
with gr.Column():
|
| 309 |
+
gr.Markdown("#### 단계 1: 프로그램 목록 조회")
|
| 310 |
+
with gr.Row(variant='compact'): # 컴포넌트 간 간격 좁힘
|
| 311 |
+
get_programs_btn = gr.Button("목록 새로고침", variant="secondary")
|
| 312 |
+
gr.Markdown(f"<small> (네트워크 상태에 따라 최대 {REQUEST_TIMEOUT}초 소요될 수 있습니다)</small>", visible=True) # 안내 문구
|
| 313 |
+
# HTML 결과 표시 영역
|
| 314 |
+
programs_result = gr.HTML(label="등록된 프로그램 목록")
|
| 315 |
+
# 참고용/숨겨진 드롭다운 (UI에는 보이지 않음)
|
| 316 |
+
program_dropdown_display = gr.Dropdown(label="내부용", visible=False)
|
| 317 |
|
| 318 |
+
with gr.Column():
|
| 319 |
+
gr.Markdown("#### 단계 2: 프로그램 선택 및 실행")
|
| 320 |
+
# 실제 사용자가 선택할 드롭다운
|
| 321 |
+
program_dropdown_exec = gr.Dropdown(
|
| 322 |
+
label="실행할 프로그램 선택",
|
| 323 |
+
choices=[], # 초기값은 비어있음
|
| 324 |
+
interactive=True,
|
| 325 |
+
info="위 '목록 새로고침' 버튼을 눌러 목록을 가져온 후 선택하세요." # Dropdown 아래 안내 문구
|
| 326 |
+
)
|
| 327 |
+
execute_program_btn = gr.Button("선택한 프로그램 실행", variant="primary") # 주요 실행 버튼 강조
|
| 328 |
# 실행 결과 표시 영역
|
| 329 |
execute_result = gr.HTML(label="실행 결과")
|
|
|
|
| 330 |
|
| 331 |
|
| 332 |
+
# --- 이벤트 핸들러 연결 ---
|
| 333 |
+
connect_btn.click(fn=connect_server, inputs=ngrok_url_input, outputs=connection_status)
|
| 334 |
+
check_status_btn.click(fn=check_status, inputs=None, outputs=status_result)
|
| 335 |
+
echo_btn.click(fn=echo_test, inputs=echo_message, outputs=echo_result)
|
| 336 |
|
| 337 |
+
# 프로그램 목록 새로고침 버튼 클릭
|
|
|
|
|
|
|
| 338 |
get_programs_btn.click(
|
| 339 |
fn=get_programs,
|
| 340 |
inputs=None,
|
| 341 |
+
# outputs 순서: HTML 결과, 숨겨진 드롭다운, 실행용 드롭다운
|
| 342 |
+
outputs=[programs_result, program_dropdown_display, program_dropdown_exec]
|
| 343 |
)
|
| 344 |
|
| 345 |
+
# 실행할 프로그램 드롭다운 선택 변경 시 (선택 사항: 결과 영역에 즉시 피드백)
|
| 346 |
+
# program_dropdown_exec.change(
|
| 347 |
+
# fn=lambda choice: f"<div style='color: blue; padding: 5px;'>'{choice}' 프로그램이 선택되었습니다. 실행 버튼을 클릭하세요.</div>" if choice else "",
|
| 348 |
+
# inputs=program_dropdown_exec,
|
| 349 |
+
# outputs=execute_result
|
| 350 |
+
# )
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
+
# 프로그램 실행 버튼 클릭
|
| 353 |
execute_program_btn.click(
|
| 354 |
fn=execute_program,
|
| 355 |
+
inputs=[program_dropdown_exec], # 선택된 드롭다운 값을 입력으로 사용
|
| 356 |
+
outputs=[execute_result] # 실행 결과를 HTML 영역에 표시
|
| 357 |
)
|
| 358 |
|
| 359 |
+
# --- 앱 실행 ---
|
| 360 |
if __name__ == "__main__":
|
| 361 |
+
print("==============================================")
|
| 362 |
+
print(" LocalPCAgent 웹 인터페이스 (Gradio 4.x) 시작 ")
|
| 363 |
+
print("==============================================")
|
| 364 |
+
print("* 서버 URL을 입력하고 '서버 연결'을 먼저 누르세요.")
|
| 365 |
+
print("* 문제가 발생하면 터미널 로그를 확인하세요.")
|
| 366 |
+
print("* Hugging Face Spaces 배포 시 requirements.txt 에")
|
| 367 |
+
print(" 'gradio>=4.0' 와 'requests' 를 포함해야 합니다.")
|
| 368 |
+
print("----------------------------------------------")
|
| 369 |
+
# share=True 옵션은 외부 접속 링크 생성 (로컬 테스트 시 불필요하면 제거)
|
| 370 |
demo.launch(debug=True, show_error=True)
|
| 371 |
+
print("==============================================")
|
| 372 |
+
print(" 웹 인터페이스 종료됨 ")
|
| 373 |
+
print("==============================================")
|