Spaces:
Sleeping
Sleeping
이정수/품질관리팀 commited on
Commit ·
79e1e8d
1
Parent(s): 6c97837
simplize'
Browse files
app.py
CHANGED
|
@@ -7,22 +7,16 @@ import time
|
|
| 7 |
# 전역 변수로 서버 URL 저장
|
| 8 |
SERVER_URL = ""
|
| 9 |
# 타임아웃 설정 (초)
|
| 10 |
-
REQUEST_TIMEOUT = 15
|
| 11 |
|
| 12 |
-
# 영구적인 HTTP 세션 생성
|
| 13 |
session = requests.Session()
|
| 14 |
-
# 연결 풀링 설정 및 자동 재시도 강화
|
| 15 |
adapter = requests.adapters.HTTPAdapter(
|
| 16 |
pool_connections=5,
|
| 17 |
pool_maxsize=10,
|
| 18 |
max_retries=requests.adapters.Retry(
|
| 19 |
-
total=3,
|
| 20 |
-
|
| 21 |
-
status_forcelist=[429, 500, 502, 503, 504], # 재시도할 HTTP 상태 코드
|
| 22 |
-
allowed_methods=["GET", "POST"], # 재시도할 HTTP 메서드
|
| 23 |
-
connect=3, # 연결 오류에 대한 재시도 횟수
|
| 24 |
-
read=3, # 읽기 오류에 대한 재시도 횟수
|
| 25 |
-
redirect=3 # 리다이렉트 재시도 횟수
|
| 26 |
)
|
| 27 |
)
|
| 28 |
session.mount('http://', adapter)
|
|
@@ -31,21 +25,12 @@ session.mount('https://', adapter)
|
|
| 31 |
def connect_server(ngrok_url):
|
| 32 |
"""서버 연결 시도"""
|
| 33 |
global SERVER_URL
|
| 34 |
-
|
| 35 |
try:
|
| 36 |
-
|
| 37 |
-
if
|
| 38 |
-
return "서버 URL을 입력해주세요!"
|
| 39 |
-
|
| 40 |
-
# 마지막 슬래시 제거
|
| 41 |
-
if ngrok_url.endswith('/'):
|
| 42 |
-
ngrok_url = ngrok_url[:-1]
|
| 43 |
-
|
| 44 |
-
# 서버 상태 확인
|
| 45 |
response = session.get(f"{ngrok_url}/health", timeout=REQUEST_TIMEOUT)
|
| 46 |
-
|
| 47 |
if response.status_code == 200:
|
| 48 |
-
SERVER_URL = ngrok_url
|
| 49 |
return "서버 연결 성공!"
|
| 50 |
else:
|
| 51 |
return f"서버 응답 오류: HTTP {response.status_code}"
|
|
@@ -55,13 +40,9 @@ def connect_server(ngrok_url):
|
|
| 55 |
def check_status():
|
| 56 |
"""서버 상태 확인"""
|
| 57 |
global SERVER_URL
|
| 58 |
-
|
| 59 |
-
if not SERVER_URL:
|
| 60 |
-
return "서버 URL이 설정되지 않았습니다. 먼저 서버에 연결하세요."
|
| 61 |
-
|
| 62 |
try:
|
| 63 |
response = session.get(f"{SERVER_URL}/api/status", timeout=REQUEST_TIMEOUT)
|
| 64 |
-
|
| 65 |
if response.status_code == 200:
|
| 66 |
return json.dumps(response.json(), indent=2, ensure_ascii=False)
|
| 67 |
else:
|
|
@@ -72,27 +53,11 @@ def check_status():
|
|
| 72 |
def echo_test(message):
|
| 73 |
"""에코 테스트"""
|
| 74 |
global SERVER_URL
|
| 75 |
-
|
| 76 |
-
if not
|
| 77 |
-
return "서버 URL이 설정되지 않았습니다. 먼저 서버에 연결하세요."
|
| 78 |
-
|
| 79 |
-
if not message:
|
| 80 |
-
return "메시지를 입력해주세요."
|
| 81 |
-
|
| 82 |
try:
|
| 83 |
-
payload = {
|
| 84 |
-
|
| 85 |
-
"data": {"message": message},
|
| 86 |
-
"timestamp": int(time.time() * 1000)
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
response = session.post(
|
| 90 |
-
f"{SERVER_URL}/api/send",
|
| 91 |
-
json=payload,
|
| 92 |
-
headers={"Content-Type": "application/json"},
|
| 93 |
-
timeout=REQUEST_TIMEOUT
|
| 94 |
-
)
|
| 95 |
-
|
| 96 |
if response.status_code == 200:
|
| 97 |
return json.dumps(response.json(), indent=2, ensure_ascii=False)
|
| 98 |
else:
|
|
@@ -100,531 +65,198 @@ def echo_test(message):
|
|
| 100 |
except RequestException as e:
|
| 101 |
return f"에코 테스트 중 오류 발생: {str(e)}"
|
| 102 |
|
|
|
|
| 103 |
def get_programs():
|
| 104 |
-
"""프로그램 목록 조회"""
|
| 105 |
global SERVER_URL
|
| 106 |
-
|
| 107 |
-
# 디버그 정보 기록
|
| 108 |
print(f"[DEBUG] 프로그램 목록 조회 함수 시작")
|
|
|
|
| 109 |
|
| 110 |
# 서버 URL이 없을 경우 테스트 데이터 사용
|
| 111 |
if not SERVER_URL:
|
| 112 |
print(f"[DEBUG] 서버 URL이 설정되지 않았습니다. 테스트 데이터를 사용합니다.")
|
| 113 |
-
# 테스트 프로그램 데이터
|
| 114 |
test_programs = [
|
| 115 |
{"id": "test1", "name": "테스트 프로그램 1", "description": "테스트 설명 1", "path": "C:\\test\\program1.exe"},
|
| 116 |
{"id": "test2", "name": "테스트 프로그램 2", "description": "테스트 설명 2", "path": "C:\\test\\program2.exe"}
|
| 117 |
]
|
| 118 |
-
|
| 119 |
-
# HTML 생성: 드롭다운 대신 버튼 목록 사용
|
| 120 |
html_output = "<div style='padding: 15px; background-color: #fffaeb; border-radius: 5px; border-left: 5px solid #ffcc00;'>"
|
| 121 |
-
html_output += "<h3 style='margin-top: 0; color: #856404;'>서버 연결 필요</h3>"
|
| 122 |
-
html_output += "<p>서버에 연결되지 않았습니다.
|
| 123 |
-
html_output += "<
|
| 124 |
-
|
| 125 |
-
# 프로그램 목록을 버튼으로 표시
|
| 126 |
-
html_output += "<div class='program-buttons' style='margin-top: 15px;'>"
|
| 127 |
for program in test_programs:
|
| 128 |
program_id = program.get("id", "")
|
| 129 |
program_name = program.get("name", "Unknown Program")
|
| 130 |
program_desc = program.get("description", "")
|
| 131 |
-
#
|
| 132 |
-
|
| 133 |
-
html_output += f"style='
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
html_output += "<script>"
|
| 139 |
-
html_output += "function selectProgram(programId) {"
|
| 140 |
-
html_output += " var resultDiv = document.getElementById('selected_program_result');"
|
| 141 |
-
html_output += " resultDiv.innerHTML = '<div style=\"padding: 15px; background-color: #d4edda; border-radius: 5px; border-left: 5px solid #28a745;\">'"
|
| 142 |
-
html_output += " + '<h3 style=\"margin-top: 0; color: #155724;\">\uD504\uB85C\uADF8\uB7A8 \uC120\uD0DD\uB428</h3>'"
|
| 143 |
-
html_output += " + '<p>\uC120\uD0DD\uD55C \uD504\uB85C\uADF8\uB7A8 ID: ' + programId + '</p>'"
|
| 144 |
-
html_output += " + '<p>\uB9C1\uD06C\uB97C \uD074\uB9AD\uD558\uC5EC \uC2E4\uD589: <a href=\"#\" onclick=\"executeProgram(\\'' + programId + '\\')\">\uD504\uB85C\uADF8\uB7A8 \uC2E4\uD589</a></p>'"
|
| 145 |
-
html_output += " + '</div>';"
|
| 146 |
-
html_output += "};"
|
| 147 |
-
html_output += "function executeProgram(programId) {"
|
| 148 |
-
html_output += " var resultDiv = document.getElementById('selected_program_result');"
|
| 149 |
-
html_output += " resultDiv.innerHTML = '<div style=\"padding: 15px; background-color: #d4edda; border-radius: 5px; border-left: 5px solid #28a745;\">'"
|
| 150 |
-
html_output += " + '<h3 style=\"margin-top: 0; color: #155724;\">\uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uC131\uACF5</h3>'"
|
| 151 |
-
html_output += " + '<p>\uD14C\uC2A4\uD2B8 \uD504\uB85C\uADF8\uB7A8 ' + programId + '\uAC00 \uC2DC\uBBAC\uB808\uC774\uC158\uB418\uC5C8\uC2B5\uB2C8\uB2E4.</p>'"
|
| 152 |
-
html_output += " + '<p><strong>\uC8FC\uC758:</strong> \uD604\uC7AC \uD14C\uC2A4\uD2B8 \uBAA8\uB4DC\uC785\uB2C8\uB2E4. \uC2E4\uC81C \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD558\uC5EC \uC2E4\uD589\uD558\uC2DC\uB824\uBA74 \uC11C\uBC84 URL\uC744 \uC785\uB825\uD558\uACE0 \uC5F0\uACB0\uD574\uC8FC\uC138\uC694.</p>'"
|
| 153 |
-
html_output += " + '</div>';"
|
| 154 |
-
html_output += "};"
|
| 155 |
-
html_output += "</script>"
|
| 156 |
-
|
| 157 |
-
# 선택된 프로그램 결과를 표시할 div 추가
|
| 158 |
-
html_output += "<div id='selected_program_result' style='margin-top: 15px;'></div>"
|
| 159 |
-
html_output += "</div>"
|
| 160 |
-
|
| 161 |
-
# 테스트 드롭다운 옵션 생성
|
| 162 |
-
program_options = []
|
| 163 |
-
for program in test_programs:
|
| 164 |
-
program_id = program.get("id", "")
|
| 165 |
-
program_name = program.get("name", "Unknown Program")
|
| 166 |
-
program_desc = program.get("description", "")
|
| 167 |
-
program_options.append((f"{program_name} - {program_desc}", program_id))
|
| 168 |
-
|
| 169 |
-
# 테스트 모드의 경우에는 드롭다운 옵션만 반환 (HTML은 위에서 생성)
|
| 170 |
-
return html_output, program_options
|
| 171 |
|
| 172 |
# 서버 URL이 있는 경우 API 호출
|
| 173 |
-
print(f"[DEBUG]
|
| 174 |
-
|
| 175 |
try:
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
"User-Agent": "LocalPCAgent-Client/1.0",
|
| 179 |
-
"Accept": "application/json",
|
| 180 |
-
"Cache-Control": "no-cache",
|
| 181 |
-
"Connection": "keep-alive"
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
# API 요청 실행
|
| 185 |
-
print(f"[DEBUG] API 요청 시도 - 타임아웃: {REQUEST_TIMEOUT}")
|
| 186 |
-
response = session.get(
|
| 187 |
-
f"{SERVER_URL}/api/programs",
|
| 188 |
-
headers=headers,
|
| 189 |
-
timeout=REQUEST_TIMEOUT
|
| 190 |
-
)
|
| 191 |
-
|
| 192 |
print(f"[DEBUG] API 응답 받음 - 상태코드: {response.status_code}")
|
| 193 |
|
| 194 |
if response.status_code == 200:
|
| 195 |
result_json = response.json()
|
| 196 |
programs = result_json.get("programs", [])
|
| 197 |
-
|
| 198 |
-
program_options = []
|
| 199 |
-
|
| 200 |
-
# 프로그램 목록에서 드롭다운 옵션 생성 (Gradio와의 연동을 위해 필요함)
|
| 201 |
-
for program in programs:
|
| 202 |
-
program_id = program.get("id", "")
|
| 203 |
-
program_name = program.get("name", "Unknown Program")
|
| 204 |
-
program_desc = program.get("description", "")
|
| 205 |
-
program_options.append((f"{program_name} - {program_desc}", program_id))
|
| 206 |
-
|
| 207 |
-
# 디버그 정보 추가
|
| 208 |
-
print(f"Found {len(program_options)} programs: {program_options}")
|
| 209 |
-
|
| 210 |
-
# 예쁘게 출력하기 위한 HTML 생성
|
| 211 |
html_output = "<div style='max-height: 400px; overflow-y: auto;'>"
|
| 212 |
-
|
| 213 |
-
#
|
| 214 |
-
html_output += "<div class='program-buttons' style='margin-top: 15px; margin-bottom: 15px;'>"
|
| 215 |
-
html_output += "<h4>프로그램 선택:</h4>"
|
| 216 |
for program in programs:
|
| 217 |
program_id = program.get("id", "")
|
| 218 |
program_name = program.get("name", "Unknown Program")
|
| 219 |
program_desc = program.get("description", "")
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
html_output += f"{program_name}</button>"
|
| 223 |
-
html_output += "</div>"
|
| 224 |
-
|
| 225 |
-
# 선택된 프로그램 정보를 표시할 영역 추가
|
| 226 |
-
html_output += "<div id='selected_program_result' style='margin-top: 15px; margin-bottom: 15px;'></div>"
|
| 227 |
-
|
| 228 |
-
# JavaScript 기능 추가
|
| 229 |
-
html_output += "<script>"
|
| 230 |
-
html_output += "function selectProgram(programId) {"
|
| 231 |
-
html_output += " console.log('프로그램 선택됨: ' + programId);"
|
| 232 |
-
html_output += " // 선택된 프로그램 정보 표시"
|
| 233 |
-
html_output += " var resultDiv = document.getElementById('selected_program_result');"
|
| 234 |
-
html_output += " if (resultDiv) {"
|
| 235 |
-
# 현재 프로그램 정보 찾기 (JavaScript 배열로 변환)
|
| 236 |
-
programs_js = json.dumps(programs) # Python 리스트를 JSON 문자열로 변환
|
| 237 |
-
html_output += f" var programs = {programs_js};\n"
|
| 238 |
-
html_output += " var programName = 'Unknown';"
|
| 239 |
-
html_output += " var programDesc = '';"
|
| 240 |
-
html_output += " var programPath = '';"
|
| 241 |
-
html_output += " for (var i = 0; i < programs.length; i++) {"
|
| 242 |
-
html_output += " if (programs[i].id === programId) {"
|
| 243 |
-
html_output += " programName = programs[i].name || 'Unknown';"
|
| 244 |
-
html_output += " programDesc = programs[i].description || '';"
|
| 245 |
-
html_output += " programPath = programs[i].path || '';"
|
| 246 |
-
html_output += " break;"
|
| 247 |
-
html_output += " }"
|
| 248 |
-
html_output += " }"
|
| 249 |
-
html_output += " resultDiv.innerHTML = '<div style=\"padding: 15px; background-color: #d4edda; border-radius: 5px; border-left: 5px solid #28a745;\">'"
|
| 250 |
-
html_output += " + '<h3 style=\"margin-top: 0; color: #155724;\">프로그램 선택됨</h3>'"
|
| 251 |
-
html_output += " + '<p><strong>프로그램:</strong> ' + programName + '</p>'"
|
| 252 |
-
html_output += " + '<p><strong>설명:</strong> ' + programDesc + '</p>'"
|
| 253 |
-
html_output += " + '<p><strong>경로:</strong> ' + programPath + '</p>'"
|
| 254 |
-
html_output += " + '<button onclick=\"executeSelectedProgram()\" style=\"margin-top: 10px; padding: 8px 15px; background-color: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;\">프로그램 실행</button>'"
|
| 255 |
-
html_output += " + '</div>';"
|
| 256 |
-
html_output += " } else {"
|
| 257 |
-
html_output += " console.error('결과를 표시할 요소를 찾을 수 없습니다.');"
|
| 258 |
-
html_output += " }"
|
| 259 |
-
html_output += " // 드롭다운 값 설정 (Gradio 연동)"
|
| 260 |
-
html_output += " var programDropdown = document.querySelector('select[data-testid*=\"program_dropdown\"]');" # Gradio 4.x+ 대응 시 data-testid 확인 필요
|
| 261 |
-
html_output += " if (programDropdown) {"
|
| 262 |
-
html_output += " console.log('드롭다운을 찾음, 값 설정 시도: ' + programId);"
|
| 263 |
-
html_output += " programDropdown.value = programId;"
|
| 264 |
-
html_output += " // 변경 이벤트 발생시키기 (Gradio 4.x+ 에서는 더 복잡할 수 있음)"
|
| 265 |
-
html_output += " var event = new Event('change', { 'bubbles': true });"
|
| 266 |
-
html_output += " programDropdown.dispatchEvent(event);"
|
| 267 |
-
html_output += " } else {"
|
| 268 |
-
html_output += " console.error('프로그램 드롭다운을 찾을 수 없습니다');"
|
| 269 |
-
html_output += " }"
|
| 270 |
-
html_output += "};"
|
| 271 |
-
html_output += "function executeSelectedProgram() {"
|
| 272 |
-
html_output += " console.log('실행 버튼 클릭됨');"
|
| 273 |
-
html_output += " // Gradio 실행 버튼 찾아서 클릭 시도 (여러 선택자 사용)"
|
| 274 |
-
html_output += " var executeBtn = document.querySelector('button[data-testid*=\"execute_program_btn\"]');" # Gradio 4.x+ 대응 시 data-testid 확인 필요
|
| 275 |
-
html_output += " if (!executeBtn) {"
|
| 276 |
-
html_output += " executeBtn = Array.from(document.querySelectorAll('button.gr-button-primary')).find(el => el.textContent.includes('프로그램 실행'));" # Gradio 4.x+ 스타일 가정
|
| 277 |
-
html_output += " }"
|
| 278 |
-
html_output += " if (!executeBtn) {"
|
| 279 |
-
html_output += " executeBtn = Array.from(document.querySelectorAll('button')).find(el => el.textContent.includes('프로그램 실행'));" # 일반 버튼 텍스트 검색
|
| 280 |
-
html_output += " }"
|
| 281 |
-
html_output += " if (executeBtn) {"
|
| 282 |
-
html_output += " console.log('실행 버튼을 찾음, 클릭 시도');"
|
| 283 |
-
html_output += " setTimeout(function() { executeBtn.click(); }, 300); // 클릭 타이밍 조정"
|
| 284 |
-
html_output += " } else {"
|
| 285 |
-
html_output += " console.error('실행 버튼을 찾을 수 없습니다');"
|
| 286 |
-
html_output += " alert('프로그램이 선택되었습니다. Gradio 인터페이스의 드롭다운에서 현재 프로그램이 선택되어 있는지 확인한 후 프로그램 실행 버튼을 클릭해 주세요.');"
|
| 287 |
-
html_output += " }"
|
| 288 |
-
html_output += "};"
|
| 289 |
-
html_output += "</script>"
|
| 290 |
-
|
| 291 |
-
# 테이블 형식으로 프로그램 목록 표시
|
| 292 |
-
html_output += "<table style='width: 100%; border-collapse: collapse;'>"
|
| 293 |
-
html_output += "<tr style='background-color: #f2f2f2;'><th style='padding: 8px; text-align: left; border-bottom: 1px solid #ddd;'>이름</th><th style='padding: 8px; text-align: left; border-bottom: 1px solid #ddd;'>설명</th><th style='padding: 8px; text-align: left; border-bottom: 1px solid #ddd;'>경로</th></tr>"
|
| 294 |
-
|
| 295 |
-
for program in programs:
|
| 296 |
html_output += f"<tr style='border-bottom: 1px solid #ddd;'>"
|
| 297 |
-
html_output += f"<td style='padding: 8px;'>{
|
| 298 |
-
html_output += f"<td style='padding: 8px;'>{
|
| 299 |
html_output += f"<td style='padding: 8px;'>{program.get('path', '')}</td>"
|
| 300 |
html_output += "</tr>"
|
| 301 |
-
|
| 302 |
html_output += "</table></div>"
|
| 303 |
-
|
| 304 |
html_output += f"<div style='margin-top: 10px; font-size: 0.9em; color: #666;'>총 {len(programs)}개 프로그램 발견</div>"
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
html_output += "<
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
html_output
|
| 311 |
-
|
| 312 |
-
return html_output, program_options
|
| 313 |
else:
|
| 314 |
-
error_html = f"<div style='padding: 15px; background-color: #f8d7da; border-radius: 5px; border-left: 5px solid #dc3545;'>"
|
| 315 |
-
|
| 316 |
-
error_html
|
| 317 |
-
error_html += f"<p>{response.text}</p>"
|
| 318 |
-
error_html += "</div>"
|
| 319 |
-
return error_html, []
|
| 320 |
except RequestException as e:
|
| 321 |
-
# 오류 상세 로그
|
| 322 |
print(f"[DEBUG] API 요청 오류: {str(e)}")
|
| 323 |
error_type = type(e).__name__
|
| 324 |
error_detail = str(e)
|
| 325 |
-
|
| 326 |
-
# 테스트 드롭다운 데이터 생성 (오류 시에도 테스트 가능하게)
|
| 327 |
test_programs = [
|
| 328 |
-
{"id": "test1", "name": "테스트
|
| 329 |
-
{"id": "test2", "name": "테스트
|
| 330 |
]
|
| 331 |
-
|
| 332 |
-
# 드롭다운 옵션 생성
|
| 333 |
-
program_options = []
|
| 334 |
-
for program in test_programs:
|
| 335 |
-
program_id = program.get("id", "")
|
| 336 |
-
program_name = program.get("name", "Unknown Program")
|
| 337 |
-
program_desc = program.get("description", "")
|
| 338 |
-
program_options.append((f"{program_name} - {program_desc}", program_id))
|
| 339 |
-
|
| 340 |
-
print(f"[DEBUG] 테스트 용 드롭다운 옵션 추가: {program_options}")
|
| 341 |
-
|
| 342 |
-
# 테스트 데이터를 기반으로 영역에 버튼 추가
|
| 343 |
-
test_html = "<div class='program-buttons' style='margin-top: 15px;'>"
|
| 344 |
-
test_html += "<h4>테스트용 프로그램 선택:</h4>"
|
| 345 |
for program in test_programs:
|
| 346 |
-
|
| 347 |
-
program_name = program.get("name", "Unknown Program")
|
| 348 |
-
program_desc = program.get("description", "")
|
| 349 |
-
test_html += f"<button onclick='selectProgram(\"{program_id}\")' "
|
| 350 |
-
test_html += f"style='margin: 5px; padding: 8px 15px; background-color: #4a6ee0; color: white; border: none; border-radius: 5px; cursor: pointer;'>"
|
| 351 |
-
test_html += f"{program_name} - {program_desc}</button><br>"
|
| 352 |
-
test_html += "</div>"
|
| 353 |
-
|
| 354 |
-
# JavaScript 기능 추가
|
| 355 |
-
test_html += "<script>"
|
| 356 |
-
test_html += "function selectProgram(programId) {"
|
| 357 |
-
test_html += " console.log('프로그램 선택됨: ' + programId);"
|
| 358 |
-
test_html += " var resultDiv = document.getElementById('selected_program_result');"
|
| 359 |
-
test_html += " if (resultDiv) {"
|
| 360 |
-
test_html += " resultDiv.innerHTML = '<div style=\"padding: 15px; background-color: #d4edda; border-radius: 5px; border-left: 5px solid #28a745;\">'"
|
| 361 |
-
test_html += " + '<h3 style=\"margin-top: 0; color: #155724;\">\uD504\uB85C\uADF8\uB7A8 \uC120\uD0DD\uB428</h3>'"
|
| 362 |
-
test_html += " + '<p>\uC120\uD0DD\uD55C \uD504\uB85C\uADF8\uB7A8 ID: ' + programId + '</p>'"
|
| 363 |
-
test_html += " + '<p>\uB9C1\uD06C\uB97C \uD074\uB9AD\uD558\uC5EC \uC2E4\uD589: <a href=\"#\" onclick=\"executeProgram(\\'' + programId + '\\')\">\uD504\uB85C\uADF8\uB7A8 \uC2E4\uD589</a></p>'"
|
| 364 |
-
test_html += " + '</div>';"
|
| 365 |
-
test_html += " } else {"
|
| 366 |
-
test_html += " console.error('결과를 표시할 요소를 찾을 수 없습니다.');"
|
| 367 |
-
test_html += " }"
|
| 368 |
-
test_html += " // 선택한 프로그램 ID를 드롭다운에도 설정 (Gradio와 연동)"
|
| 369 |
-
test_html += " var programDropdown = document.querySelector('select[data-testid*=\"program_dropdown\"]');" # Gradio 4.x+ 대응 시 data-testid 확인 필요
|
| 370 |
-
test_html += " if (programDropdown) {"
|
| 371 |
-
test_html += " console.log('드롭다운을 찾음, 값 설정 시도: ' + programId);"
|
| 372 |
-
test_html += " programDropdown.value = programId;"
|
| 373 |
-
test_html += " // 변경 이벤트 발생시키기 (Gradio 4.x+ 에서는 더 복잡할 수 있음)"
|
| 374 |
-
test_html += " var event = new Event('change', { 'bubbles': true });"
|
| 375 |
-
test_html += " programDropdown.dispatchEvent(event);"
|
| 376 |
-
test_html += " } else {"
|
| 377 |
-
test_html += " console.error('프로그램 드롭다운을 찾을 수 없습니다');"
|
| 378 |
-
test_html += " }"
|
| 379 |
-
test_html += "};"
|
| 380 |
-
test_html += "function executeProgram(programId) {"
|
| 381 |
-
test_html += " console.log('프로그램 실행: ' + programId);"
|
| 382 |
-
test_html += " var resultDiv = document.getElementById('selected_program_result');"
|
| 383 |
-
test_html += " if (resultDiv) {"
|
| 384 |
-
test_html += " resultDiv.innerHTML = '<div style=\"padding: 15px; background-color: #d4edda; border-radius: 5px; border-left: 5px solid #28a745;\">'"
|
| 385 |
-
test_html += " + '<h3 style=\"margin-top: 0; color: #155724;\">\uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uC131\uACF5</h3>'"
|
| 386 |
-
test_html += " + '<p>\uD14C\uC2A4\uD2B8 \uD504\uB85C\uADF8\uB7A8 ' + programId + '\uAC00 \uC2DC\uBBAC\uB808\uC774\uC158\uB418\uC5C8\uC2B5\uB2C8\uB2E4.</p>'"
|
| 387 |
-
test_html += " + '<p><strong>\uC8FC\uC758:</strong> \uD604\uC7AC \uD14C\uC2A4\uD2B8 \uBAA8\uB4DC\uC785\uB2C8\uB2E4. \uC2E4\uC81C \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD558\uC5EC \uC2E4\uD589\uD558\uC2DC\uB824\uBA74 \uC11C\uBC84 URL\uC744 \uC785\uB825\uD558\uACE0 \uC5F0\uACB0\uD574\uC8FC\uC138\uC694.</p>'"
|
| 388 |
-
test_html += " + '</div>';"
|
| 389 |
-
test_html += " } else {"
|
| 390 |
-
test_html += " console.error('결과를 표시할 요소를 찾을 수 없습니다.');"
|
| 391 |
-
test_html += " }"
|
| 392 |
-
test_html += " // Gradio 인터페이스의 실행 버튼에 연동"
|
| 393 |
-
test_html += " var executeBtn = document.querySelector('button[data-testid*=\"execute_program_btn\"]');" # Gradio 4.x+ 대응 시 data-testid 확인 필요
|
| 394 |
-
test_html += " if (!executeBtn) {"
|
| 395 |
-
test_html += " executeBtn = Array.from(document.querySelectorAll('button.gr-button-primary')).find(el => el.textContent.includes('프로그램 실행'));" # Gradio 4.x+ 스타일 가정
|
| 396 |
-
test_html += " }"
|
| 397 |
-
test_html += " if (!executeBtn) {"
|
| 398 |
-
test_html += " executeBtn = Array.from(document.querySelectorAll('button')).find(el => el.textContent.includes('프로그램 실행'));" # 일반 버튼 텍스트 검색
|
| 399 |
-
test_html += " }"
|
| 400 |
-
test_html += " if (executeBtn) {"
|
| 401 |
-
test_html += " console.log('실행 버튼을 찾음, 클릭 시도');"
|
| 402 |
-
test_html += " setTimeout(function() { executeBtn.click(); }, 300);" # 클릭 타이밍 조정
|
| 403 |
-
test_html += " } else {"
|
| 404 |
-
test_html += " console.log('실행 버튼을 찾을 수 없습니다. 테스트 실행을 직접 진행합니다.');"
|
| 405 |
-
test_html += " }"
|
| 406 |
-
test_html += "};"
|
| 407 |
-
test_html += "</script>"
|
| 408 |
|
| 409 |
-
#
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
if "timeout" in error_detail.lower() or "timed out" in error_detail.lower():
|
| 414 |
-
error_html = f"<div style='padding: 15px; background-color: #f8d7da; border-radius: 5px; border-left: 5px solid #dc3545;'>"
|
| 415 |
-
error_html += f"<h3 style='margin-top: 0; color: #721c24;'>프로그램 목록 조회 타임아웃</h3>"
|
| 416 |
-
error_html += f"<p>서버가 {REQUEST_TIMEOUT}초 내에 응답하지 않았습니다.</p>"
|
| 417 |
-
error_html += f"<p><strong>해결 방법:</strong> 서버 연결을 다시 확인하고, ngrok을 다시 시작해보세요.</p>"
|
| 418 |
-
error_html += f"<p><strong>오류 정보:</strong> {error_type} - {error_detail}</p>"
|
| 419 |
-
error_html += f"<p style='margin-top: 15px;'><strong>테스트 모드:</strong> 임시 테스트용 데이터가 로드되었습니다.</p>"
|
| 420 |
-
error_html += test_html # 테스트 HTML 포함
|
| 421 |
-
error_html += "</div>"
|
| 422 |
-
return error_html, program_options
|
| 423 |
-
else:
|
| 424 |
-
error_html = f"<div style='padding: 15px; background-color: #f8d7da; border-radius: 5px; border-left: 5px solid #dc3545;'>"
|
| 425 |
-
error_html += f"<h3 style='margin-top: 0; color: #721c24;'>프로그램 목록 조회 중 오류 발생</h3>"
|
| 426 |
-
error_html += f"<p>{str(e)}</p>"
|
| 427 |
-
error_html += f"<p><strong>오류 유형:</strong> {error_type}</p>"
|
| 428 |
-
error_html += f"<p><strong>해결 방법:</strong> 서버 연���을 확인하고 다시 시도해보세요.</p>"
|
| 429 |
-
error_html += f"<p style='margin-top: 15px;'><strong>테스트 모드:</strong> 임시 테스트용 데이터가 로드되었습니다.</p>"
|
| 430 |
-
error_html += test_html # 테스트 HTML 포함
|
| 431 |
-
error_html += "</div>"
|
| 432 |
-
|
| 433 |
-
return error_html, program_options
|
| 434 |
|
|
|
|
| 435 |
def execute_program(program_id):
|
| 436 |
"""프로그램 실행"""
|
| 437 |
global SERVER_URL
|
| 438 |
-
|
| 439 |
-
# 디버그 정보 기록
|
| 440 |
print(f"[DEBUG] 프로그램 실행 시도: {program_id}")
|
|
|
|
| 441 |
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
# 테스트 데이터 확인 (테스트 데이터의 경우 테스트 성공 반환)
|
| 446 |
-
if program_id.startswith("test"):
|
| 447 |
print(f"[DEBUG] 테스트 프로그램 확인됨. 테스트 모드로 성공 응답 생성")
|
| 448 |
-
# 성공
|
| 449 |
-
html_output = "<div style='padding: 15px; background-color: #d4edda; border-radius: 5px; border-left: 5px solid #28a745;'>"
|
| 450 |
-
html_output += f"<h3 style='margin-top: 0; color: #155724;'>테스트 실행 성공</h3>"
|
| 451 |
-
html_output += f"<p style='margin-bottom: 0;'>테스트 프로그램 {program_id}가 시뮬레이션되었습니다.</p>"
|
| 452 |
-
html_output += f"<p style='margin-top: 10px;'><strong>주의:</strong> 현재 테스트 모드입니다. 실제 서버에 연결하여 실행하시려면 서버 URL을 입력하고 연결해주세요.</p>"
|
| 453 |
-
html_output += "</div>"
|
| 454 |
return html_output
|
| 455 |
|
| 456 |
if not SERVER_URL:
|
| 457 |
-
# 서버
|
| 458 |
-
html_output = "<div style='padding: 15px; background-color: #fff3cd; border-radius: 5px; border-left: 5px solid #ffc107;'>"
|
| 459 |
-
html_output += "<h3 style='margin-top: 0; color: #856404;'>서버 연결 필요</h3>"
|
| 460 |
-
html_output += "프로그램을 실행하려면 먼저 서버에 연결해야 합니다. 페이지 상단의 '서버 연결' 영역에서 ngrok URL을 입력하고 연결하세요."
|
| 461 |
-
html_output += "</div>"
|
| 462 |
-
return html_output
|
| 463 |
|
| 464 |
-
# 디버그 정보 기록
|
| 465 |
print(f"[DEBUG] 서버에 실행 요청 전송: {SERVER_URL}/api/programs/{program_id}/execute")
|
| 466 |
-
|
| 467 |
try:
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
"User-Agent": "LocalPCAgent-Client/1.0",
|
| 471 |
-
"Accept": "application/json",
|
| 472 |
-
"Content-Type": "application/json"
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
print(f"[DEBUG] API 요청 전송 - 타임아웃: {REQUEST_TIMEOUT}")
|
| 476 |
-
response = session.post(
|
| 477 |
-
f"{SERVER_URL}/api/programs/{program_id}/execute",
|
| 478 |
-
headers=headers,
|
| 479 |
-
json={"id": program_id}, # API가 JSON 본문을 예상할 수 있음
|
| 480 |
-
timeout=REQUEST_TIMEOUT
|
| 481 |
-
)
|
| 482 |
-
|
| 483 |
print(f"[DEBUG] API 응답 받음 - 상태코드: {response.status_code}")
|
| 484 |
|
| 485 |
if response.status_code == 200:
|
| 486 |
result = response.json()
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
html_output = "<
|
| 490 |
-
html_output += f"<h3 style='margin-top: 0; color: #155724;'>실행 성공</h3>"
|
| 491 |
-
html_output += f"<p style='margin-bottom: 0;'>{result.get('message', '프로그램이 성공적으로 실행되었습니다.')}</p>"
|
| 492 |
-
html_output += "</div>"
|
| 493 |
-
|
| 494 |
-
# 원본 JSON도 접이식으로 제공
|
| 495 |
-
html_output += "<details style='margin-top: 10px;'>"
|
| 496 |
-
html_output += "<summary style='cursor: pointer; color: #4a6ee0;'>원본 JSON 데이터 보기</summary>"
|
| 497 |
-
html_output += f"<pre style='background-color: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;'>{json.dumps(result, indent=2, ensure_ascii=False)}</pre>"
|
| 498 |
-
html_output += "</details>"
|
| 499 |
-
|
| 500 |
return html_output
|
| 501 |
else:
|
| 502 |
-
#
|
| 503 |
-
html_output = "<div style='padding: 15px; background-color: #f8d7da; border-radius: 5px; border-left: 5px solid #dc3545;'>"
|
| 504 |
-
html_output += f"<h3 style='margin-top: 0; color: #721c24;'>실행 실패</h3>"
|
| 505 |
-
html_output += f"<p style='margin-bottom: 0;'>HTTP 오류 {response.status_code}</p>"
|
| 506 |
-
html_output += f"<p style='margin-bottom: 0;'>{response.text}</p>"
|
| 507 |
-
html_output += "</div>"
|
| 508 |
-
|
| 509 |
-
return html_output
|
| 510 |
except RequestException as e:
|
| 511 |
-
# 오류 상세 로그
|
| 512 |
print(f"[DEBUG] 프로그램 실행 중 오류: {str(e)}")
|
| 513 |
error_type = type(e).__name__
|
| 514 |
error_detail = str(e)
|
| 515 |
-
|
| 516 |
-
# 타임아웃 오류 확인
|
| 517 |
if "timeout" in error_detail.lower() or "timed out" in error_detail.lower():
|
| 518 |
-
|
| 519 |
-
html_output += f"<h3 style='margin-top: 0; color: #721c24;'>프로그램 실행 타임아웃</h3>"
|
| 520 |
-
html_output += f"<p style='margin-bottom: 10px;'>서버가 {REQUEST_TIMEOUT}초 내에 응답하지 않았습니다.</p>"
|
| 521 |
-
html_output += f"<p><strong>해결 방법:</strong> 서버 연결을 확인하고 다시 시도해보세요. 서버가 다른 작업을 처리하고 있을 수 있습니다.</p>"
|
| 522 |
-
html_output += f"<p><strong>오류 정보:</strong> {error_type} - {error_detail}</p>"
|
| 523 |
-
html_output += "</div>"
|
| 524 |
else:
|
| 525 |
-
# 오류
|
| 526 |
-
html_output = "<div style='padding: 15px; background-color: #f8d7da; border-radius: 5px; border-left: 5px solid #dc3545;'>"
|
| 527 |
-
html_output += f"<h3 style='margin-top: 0; color: #721c24;'>오류 발생</h3>"
|
| 528 |
-
html_output += f"<p style='margin-bottom: 10px;'>{str(e)}</p>"
|
| 529 |
-
html_output += f"<p><strong>오류 유형:</strong> {error_type}</p>"
|
| 530 |
-
html_output += "</div>"
|
| 531 |
|
| 532 |
-
return html_output
|
| 533 |
|
| 534 |
-
# UI 구성
|
| 535 |
with gr.Blocks(title="LocalPCAgent 제어 인터페이스") as demo:
|
| 536 |
gr.Markdown("# LocalPCAgent 제어 인터페이스")
|
| 537 |
gr.Markdown("로컬 PC의 기능을 원격으로 제어하고 관리할 수 있는 웹 인터페이스입니다.")
|
| 538 |
|
| 539 |
-
# 서버 연결 섹션
|
| 540 |
with gr.Group():
|
| 541 |
gr.Markdown("## 서버 연결")
|
| 542 |
with gr.Row():
|
| 543 |
-
ngrok_url_input = gr.Textbox(
|
| 544 |
-
label="ngrok URL",
|
| 545 |
-
placeholder="https://xxxx-xx-xx-xxx-xx.ngrok.io",
|
| 546 |
-
value=""
|
| 547 |
-
)
|
| 548 |
connect_btn = gr.Button("연결")
|
| 549 |
-
|
| 550 |
connection_status = gr.Textbox(label="연결 상태", interactive=False)
|
| 551 |
|
| 552 |
-
# 작업 메뉴 탭
|
| 553 |
with gr.Tabs() as tabs:
|
| 554 |
-
# 1. 기본 기능 탭
|
| 555 |
with gr.TabItem("기본 기능"):
|
| 556 |
with gr.Group():
|
| 557 |
gr.Markdown("### 서버 상태 확인")
|
| 558 |
check_status_btn = gr.Button("상태 확인")
|
| 559 |
status_result = gr.Textbox(label="결과", interactive=False, lines=10)
|
| 560 |
-
|
| 561 |
with gr.Group():
|
| 562 |
gr.Markdown("### 에코 테스트")
|
| 563 |
-
echo_message = gr.Textbox(
|
| 564 |
-
label="메시지",
|
| 565 |
-
placeholder="테스트 메시지 입력",
|
| 566 |
-
value="Hello from Hugging Face!"
|
| 567 |
-
)
|
| 568 |
echo_btn = gr.Button("에코 테스트")
|
| 569 |
echo_result = gr.Textbox(label="결과", interactive=False, lines=10)
|
| 570 |
|
| 571 |
-
# 2. 프로그램 실행 탭 (이전 '장치 관리' 탭 자리에 배치)
|
| 572 |
with gr.TabItem("프로그램 실행"):
|
| 573 |
with gr.Group():
|
| 574 |
-
gr.Markdown("### 프로그램 목록 조회")
|
| 575 |
with gr.Row():
|
| 576 |
get_programs_btn = gr.Button("프로그램 목록 가져오기")
|
| 577 |
gr.Markdown(f"<p style='color:#666; font-size:0.9em;'>이 작업은 최대 {REQUEST_TIMEOUT}초가 소요될 수 있습니다.</p>")
|
| 578 |
-
# 결과 표시 영역
|
| 579 |
-
programs_result = gr.HTML(label="
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
with gr.Group():
|
| 583 |
-
gr.Markdown("### 프로그램 실행")
|
| 584 |
-
# 위에서 선언한 program_dropdown을 여기서 다시 참조하여 사용
|
| 585 |
-
program_dropdown_exec = gr.Dropdown(label="실행할 프로그램 선택", choices=[], interactive=True, value=None, elem_id="program_dropdown_exec") # 별도 elem_id 부여
|
| 586 |
-
execute_program_btn = gr.Button("프로그램 실행", variant="primary", elem_id="execute_program_btn") # elem_id 부여
|
| 587 |
-
execute_result = gr.HTML(label="결과") # HTML 결과를 표시할 컴포넌트
|
| 588 |
-
gr.Markdown("<small>* 프로그램 목록을 먼저 가져오고 선택한 후 실행하세요</small>")
|
| 589 |
|
| 590 |
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
|
|
|
|
|
|
| 597 |
|
| 598 |
-
# 기본 기능 이벤트
|
| 599 |
-
check_status_btn.click(
|
| 600 |
-
fn=check_status,
|
| 601 |
-
inputs=None,
|
| 602 |
-
outputs=[status_result]
|
| 603 |
-
)
|
| 604 |
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
)
|
| 610 |
|
| 611 |
-
# 프로그램 실행 이벤트
|
| 612 |
# '프로그램 목록 가져오기' 버튼 클릭 시:
|
| 613 |
-
#
|
| 614 |
-
#
|
| 615 |
-
# 3. program_dropdown_exec (프로그램 실행 섹션의 드롭다운)도 동일하게 업데이트
|
| 616 |
get_programs_btn.click(
|
| 617 |
fn=get_programs,
|
| 618 |
inputs=None,
|
| 619 |
-
outputs=[programs_result,
|
| 620 |
)
|
| 621 |
|
| 622 |
-
# 프로그램
|
| 623 |
program_dropdown_exec.change(
|
| 624 |
-
fn=lambda x: print(f"[DEBUG] 실행할 프로그램 선택됨
|
| 625 |
-
"<div style='padding:
|
| 626 |
-
f"<p>실행할 프로그램으로 <strong>'{x}'</strong> 가 선택되었습니다.</p>"
|
| 627 |
-
"<p>'프로그램 실행' 버튼을 클릭하여 진행하세요.</p>"
|
| 628 |
"</div>" if x else ""
|
| 629 |
),
|
| 630 |
inputs=[program_dropdown_exec],
|
|
@@ -634,17 +266,14 @@ with gr.Blocks(title="LocalPCAgent 제어 인터페이스") as demo:
|
|
| 634 |
# '프로그램 실행' 버튼 클릭 시
|
| 635 |
execute_program_btn.click(
|
| 636 |
fn=execute_program,
|
| 637 |
-
inputs=[program_dropdown_exec], #
|
| 638 |
-
outputs=[execute_result]
|
| 639 |
)
|
| 640 |
|
| 641 |
# 앱 실행
|
| 642 |
if __name__ == "__main__":
|
| 643 |
print("=== LocalPCAgent 웹 인터페이스 시작 ===\n")
|
| 644 |
-
print("*
|
| 645 |
-
print("*
|
| 646 |
-
print("* 구현된 테스트 기능을 통해 드롭다운 작동 여부를 확인합니다.\n")
|
| 647 |
-
|
| 648 |
-
# 디버그 모드로 실행하여 오류 추적이 쉽도록 함
|
| 649 |
demo.launch(debug=True, show_error=True)
|
| 650 |
print("\n=== 웹 인터페이스 종료 ===")
|
|
|
|
| 7 |
# 전역 변수로 서버 URL 저장
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
)
|
| 21 |
)
|
| 22 |
session.mount('http://', adapter)
|
|
|
|
| 25 |
def connect_server(ngrok_url):
|
| 26 |
"""서버 연결 시도"""
|
| 27 |
global SERVER_URL
|
|
|
|
| 28 |
try:
|
| 29 |
+
if not ngrok_url: return "서버 URL을 입력해주세요!"
|
| 30 |
+
if ngrok_url.endswith('/'): ngrok_url = ngrok_url[:-1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
response = session.get(f"{ngrok_url}/health", timeout=REQUEST_TIMEOUT)
|
|
|
|
| 32 |
if response.status_code == 200:
|
| 33 |
+
SERVER_URL = ngrok_url
|
| 34 |
return "서버 연결 성공!"
|
| 35 |
else:
|
| 36 |
return f"서버 응답 오류: HTTP {response.status_code}"
|
|
|
|
| 40 |
def check_status():
|
| 41 |
"""서버 상태 확인"""
|
| 42 |
global SERVER_URL
|
| 43 |
+
if not SERVER_URL: return "서버 URL이 설정되지 않았습니다. 먼저 서버에 연결하세요."
|
|
|
|
|
|
|
|
|
|
| 44 |
try:
|
| 45 |
response = session.get(f"{SERVER_URL}/api/status", timeout=REQUEST_TIMEOUT)
|
|
|
|
| 46 |
if response.status_code == 200:
|
| 47 |
return json.dumps(response.json(), indent=2, ensure_ascii=False)
|
| 48 |
else:
|
|
|
|
| 53 |
def echo_test(message):
|
| 54 |
"""에코 테스트"""
|
| 55 |
global SERVER_URL
|
| 56 |
+
if not SERVER_URL: return "서버 URL이 설정되지 않았습니다. 먼저 서버에 연결하세요."
|
| 57 |
+
if not message: return "메시지를 입력해주세요."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
return json.dumps(response.json(), indent=2, ensure_ascii=False)
|
| 63 |
else:
|
|
|
|
| 65 |
except RequestException as e:
|
| 66 |
return f"에코 테스트 중 오류 발생: {str(e)}"
|
| 67 |
|
| 68 |
+
# --- get_programs 함수 수정 ---
|
| 69 |
def get_programs():
|
| 70 |
+
"""프로그램 목록 조회 (JavaScript 제거 및 Gradio 업데이트 방식 적용)"""
|
| 71 |
global SERVER_URL
|
|
|
|
|
|
|
| 72 |
print(f"[DEBUG] 프로그램 목록 조회 함수 시작")
|
| 73 |
+
program_options = [] # 드롭다운 선택지 리스트 초기화
|
| 74 |
|
| 75 |
# 서버 URL이 없을 경우 테스트 데이터 사용
|
| 76 |
if not SERVER_URL:
|
| 77 |
print(f"[DEBUG] 서버 URL이 설정되지 않았습니다. 테스트 데이터를 사용합니다.")
|
|
|
|
| 78 |
test_programs = [
|
| 79 |
{"id": "test1", "name": "테스트 프로그램 1", "description": "테스트 설명 1", "path": "C:\\test\\program1.exe"},
|
| 80 |
{"id": "test2", "name": "테스트 프로그램 2", "description": "테스트 설명 2", "path": "C:\\test\\program2.exe"}
|
| 81 |
]
|
| 82 |
+
# HTML 생성 (JavaScript 없이 정보만 표시)
|
|
|
|
| 83 |
html_output = "<div style='padding: 15px; background-color: #fffaeb; border-radius: 5px; border-left: 5px solid #ffcc00;'>"
|
| 84 |
+
html_output += "<h3 style='margin-top: 0; color: #856404;'>서버 연결 필요 (테스트 모드)</h3>"
|
| 85 |
+
html_output += "<p>서버에 연결되지 않았습니다. 아래는 테스트용 프로그램 목록입니다.</p>"
|
| 86 |
+
html_output += "<table style='width: 100%; border-collapse: collapse; margin-top: 10px;'>"
|
| 87 |
+
html_output += "<tr style='background-color: #f2f2f2;'><th style='padding: 8px; text-align: left;'>이름</th><th style='padding: 8px; text-align: left;'>설명</th></tr>"
|
|
|
|
|
|
|
| 88 |
for program in test_programs:
|
| 89 |
program_id = program.get("id", "")
|
| 90 |
program_name = program.get("name", "Unknown Program")
|
| 91 |
program_desc = program.get("description", "")
|
| 92 |
+
# 드롭다운 옵션 생성
|
| 93 |
+
program_options.append((f"{program_name} ({program_desc})", program_id))
|
| 94 |
+
html_output += f"<tr><td style='padding: 8px;'>{program_name}</td><td style='padding: 8px;'>{program_desc}</td></tr>"
|
| 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 |
+
headers = {"User-Agent": "LocalPCAgent-Client/1.0", "Accept": "application/json", "Cache-Control": "no-cache", "Connection": "keep-alive"}
|
| 104 |
+
response = session.get(f"{SERVER_URL}/api/programs", headers=headers, timeout=REQUEST_TIMEOUT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
print(f"[DEBUG] API 응답 받음 - 상태코드: {response.status_code}")
|
| 106 |
|
| 107 |
if response.status_code == 200:
|
| 108 |
result_json = response.json()
|
| 109 |
programs = result_json.get("programs", [])
|
| 110 |
+
# HTML 생성 (테이블 형식, JavaScript 없음)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
html_output = "<div style='max-height: 400px; overflow-y: auto;'>"
|
| 112 |
+
html_output += "<table style='width: 100%; border-collapse: collapse;'>"
|
| 113 |
+
html_output += "<tr style='background-color: #f2f2f2;'><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>"
|
|
|
|
|
|
|
| 114 |
for program in programs:
|
| 115 |
program_id = program.get("id", "")
|
| 116 |
program_name = program.get("name", "Unknown Program")
|
| 117 |
program_desc = program.get("description", "")
|
| 118 |
+
# 드롭다운 옵션 생성
|
| 119 |
+
program_options.append((f"{program_name} ({program_desc})", program_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
html_output += f"<tr style='border-bottom: 1px solid #ddd;'>"
|
| 121 |
+
html_output += f"<td style='padding: 8px;'>{program_name}</td>"
|
| 122 |
+
html_output += f"<td style='padding: 8px;'>{program_desc}</td>"
|
| 123 |
html_output += f"<td style='padding: 8px;'>{program.get('path', '')}</td>"
|
| 124 |
html_output += "</tr>"
|
|
|
|
| 125 |
html_output += "</table></div>"
|
|
|
|
| 126 |
html_output += f"<div style='margin-top: 10px; font-size: 0.9em; color: #666;'>총 {len(programs)}개 프로그램 발견</div>"
|
| 127 |
+
# 원본 JSON 보기 추가
|
| 128 |
+
html_output += "<details style='margin-top: 10px;'><summary style='cursor: pointer; color: #4a6ee0;'>원본 JSON 데이터 보기</summary>"
|
| 129 |
+
html_output += f"<pre style='background-color: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;'>{json.dumps(result_json, indent=2, ensure_ascii=False)}</pre></details>"
|
| 130 |
+
print(f"[DEBUG] 발견된 프로그램 옵션: {program_options}")
|
| 131 |
+
# HTML 결과와 함께 드롭다운 업데이트 객체 반환
|
| 132 |
+
return html_output, gr.Dropdown.update(choices=program_options), gr.Dropdown.update(choices=program_options)
|
|
|
|
|
|
|
| 133 |
else:
|
| 134 |
+
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>HTTP 오류 {response.status_code}</p><p>{response.text}</p></div>"
|
| 135 |
+
# 오류 발생 시 빈 드롭다운 업데이트 반환
|
| 136 |
+
return error_html, gr.Dropdown.update(choices=[]), gr.Dropdown.update(choices=[])
|
|
|
|
|
|
|
|
|
|
| 137 |
except RequestException as e:
|
|
|
|
| 138 |
print(f"[DEBUG] API 요청 오류: {str(e)}")
|
| 139 |
error_type = type(e).__name__
|
| 140 |
error_detail = str(e)
|
| 141 |
+
# 테스트용 데이터 생성 (오류 발생 시)
|
|
|
|
| 142 |
test_programs = [
|
| 143 |
+
{"id": "test1", "name": "오류-테스트1", "description": "오류 발생 시 테스트", "path": ""},
|
| 144 |
+
{"id": "test2", "name": "오류-테스트2", "description": "오류 발생 시 테스트", "path": ""}
|
| 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 |
print(f"[DEBUG] 프로그램 실행 시도: {program_id}")
|
| 159 |
+
if not program_id: return "프로그램을 선택해주세요."
|
| 160 |
|
| 161 |
+
# 테스트 데이터 처리
|
| 162 |
+
if program_id.startswith("test") or program_id.startswith("오류-테스트"):
|
|
|
|
|
|
|
|
|
|
| 163 |
print(f"[DEBUG] 테스트 프로그램 확인됨. 테스트 모드로 성공 응답 생성")
|
| 164 |
+
html_output = "<div style='padding: 15px; background-color: #d4edda; border-radius: 5px; border-left: 5px solid #28a745;'><h3 style='margin-top: 0; color: #155724;'>테스트 실행 성공</h3><p style='margin-bottom: 0;'>테스트 프로그램 {program_id}가 시뮬레이션되었습니다.</p><p style='margin-top: 10px;'><strong>주의:</strong> 실제 서버에 연결된 상태가 아닙니다.</p></div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
return html_output
|
| 166 |
|
| 167 |
if not SERVER_URL:
|
| 168 |
+
return "<div style='padding: 15px; background-color: #fff3cd; border-radius: 5px; border-left: 5px solid #ffc107;'><h3 style='margin-top: 0; color: #856404;'>서버 연결 필요</h3><p>프로그램을 실행하려면 먼저 서버에 연결해야 합니다.</p></div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
|
|
|
| 170 |
print(f"[DEBUG] 서버에 실행 요청 전송: {SERVER_URL}/api/programs/{program_id}/execute")
|
|
|
|
| 171 |
try:
|
| 172 |
+
headers = {"User-Agent": "LocalPCAgent-Client/1.0", "Accept": "application/json", "Content-Type": "application/json"}
|
| 173 |
+
response = session.post(f"{SERVER_URL}/api/programs/{program_id}/execute", headers=headers, json={"id": program_id}, timeout=REQUEST_TIMEOUT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
print(f"[DEBUG] API 응답 받음 - 상태코드: {response.status_code}")
|
| 175 |
|
| 176 |
if response.status_code == 200:
|
| 177 |
result = response.json()
|
| 178 |
+
html_output = "<div style='padding: 15px; background-color: #d4edda; border-radius: 5px; border-left: 5px solid #28a745;'><h3 style='margin-top: 0; color: #155724;'>실행 성공</h3><p style='margin-bottom: 0;'>{result.get('message', '프로그램이 성공적으로 실행되었습니다.')}</p></div>"
|
| 179 |
+
html_output += "<details style='margin-top: 10px;'><summary style='cursor: pointer; color: #4a6ee0;'>원본 JSON 데이터 보기</summary>"
|
| 180 |
+
html_output += f"<pre style='background-color: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto;'>{json.dumps(result, indent=2, ensure_ascii=False)}</pre></details>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
return html_output
|
| 182 |
else:
|
| 183 |
+
return 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>HTTP 오류 {response.status_code}</p><p>{response.text}</p></div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
except RequestException as e:
|
|
|
|
| 185 |
print(f"[DEBUG] 프로그램 실행 중 오류: {str(e)}")
|
| 186 |
error_type = type(e).__name__
|
| 187 |
error_detail = str(e)
|
| 188 |
+
# 타임아웃 또는 일반 오류 메시지 생성 (이전과 유사)
|
|
|
|
| 189 |
if "timeout" in error_detail.lower() or "timed out" in error_detail.lower():
|
| 190 |
+
return 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>서버가 {REQUEST_TIMEOUT}초 내에 응답하지 않았습니다.</p><p><strong>오류 정보:</strong> {error_type} - {error_detail}</p></div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
else:
|
| 192 |
+
return 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></div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
|
|
|
| 194 |
|
| 195 |
+
# --- UI 구성 수정 ---
|
| 196 |
with gr.Blocks(title="LocalPCAgent 제어 인터페이스") as demo:
|
| 197 |
gr.Markdown("# LocalPCAgent 제어 인터페이스")
|
| 198 |
gr.Markdown("로컬 PC의 기능을 원격으로 제어하고 관리할 수 있는 웹 인터페이스입니다.")
|
| 199 |
|
|
|
|
| 200 |
with gr.Group():
|
| 201 |
gr.Markdown("## 서버 연결")
|
| 202 |
with gr.Row():
|
| 203 |
+
ngrok_url_input = gr.Textbox(label="ngrok URL", placeholder="https://xxxx-xx-xx-xxx-xx.ngrok.io", value="")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
connect_btn = gr.Button("연결")
|
|
|
|
| 205 |
connection_status = gr.Textbox(label="연결 상태", interactive=False)
|
| 206 |
|
|
|
|
| 207 |
with gr.Tabs() as tabs:
|
|
|
|
| 208 |
with gr.TabItem("기본 기능"):
|
| 209 |
with gr.Group():
|
| 210 |
gr.Markdown("### 서버 상태 확인")
|
| 211 |
check_status_btn = gr.Button("상태 확인")
|
| 212 |
status_result = gr.Textbox(label="결과", interactive=False, lines=10)
|
|
|
|
| 213 |
with gr.Group():
|
| 214 |
gr.Markdown("### 에코 테스트")
|
| 215 |
+
echo_message = gr.Textbox(label="메시지", placeholder="테스트 메시지 입력", value="Hello from Gradio!")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
echo_btn = gr.Button("에코 테스트")
|
| 217 |
echo_result = gr.Textbox(label="결과", interactive=False, lines=10)
|
| 218 |
|
|
|
|
| 219 |
with gr.TabItem("프로그램 실행"):
|
| 220 |
with gr.Group():
|
| 221 |
+
gr.Markdown("### 1. 프로그램 목록 조회")
|
| 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.Group():
|
| 232 |
+
gr.Markdown("### 2. 프로그램 실행")
|
| 233 |
+
# 실제 사용자가 선택할 드롭다운 - 업데이트 대상 O
|
| 234 |
+
program_dropdown_exec = gr.Dropdown(label="실행할 프로그램 선택", choices=[], interactive=True, value=None)
|
| 235 |
+
execute_program_btn = gr.Button("프로그램 실행", variant="primary")
|
| 236 |
+
# 실행 결과 표시 영역
|
| 237 |
+
execute_result = gr.HTML(label="실행 결과")
|
| 238 |
+
gr.Markdown("<small>* 위에서 '프로그램 목록 가져오기'를 먼저 실행한 후, 여기서 프로그램을 선택하고 실행하세요.</small>")
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
+
# --- 이벤트 핸들러 수정 ---
|
| 242 |
+
connect_btn.click(fn=connect_server, inputs=[ngrok_url_input], outputs=[connection_status])
|
| 243 |
+
check_status_btn.click(fn=check_status, inputs=None, outputs=[status_result])
|
| 244 |
+
echo_btn.click(fn=echo_test, inputs=[echo_message], outputs=[echo_result])
|
|
|
|
| 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=[programs_result, program_dropdown_display, program_dropdown_exec] # HTML 결과, 참고용 드롭다운, 실행용 드롭다운 순서로 업데이트
|
| 253 |
)
|
| 254 |
|
| 255 |
+
# 실행할 프로그램 드롭다운 값이 변경될 때 실행 결과 영역에 메시지 표시 (단순 확인용)
|
| 256 |
program_dropdown_exec.change(
|
| 257 |
+
fn=lambda x: print(f"[DEBUG] 실행할 프로그램 선택됨: {x}") or (
|
| 258 |
+
"<div style='padding: 10px; background-color: #e7f3ff; border-radius: 5px; border-left: 5px solid #007bff; font-size: 0.9em;'>"
|
| 259 |
+
f"<p>실행할 프로그램으로 <strong>'{x}'</strong> 가 선택되었습니다. 하단의 '프로그램 실행' 버튼을 클릭하세요.</p>"
|
|
|
|
| 260 |
"</div>" if x else ""
|
| 261 |
),
|
| 262 |
inputs=[program_dropdown_exec],
|
|
|
|
| 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("=== LocalPCAgent 웹 인터페이스 시작 ===\n")
|
| 276 |
+
print("* UI 업데이트 로직 수정됨 (JavaScript 제거, Gradio 표준 방식 적용).")
|
| 277 |
+
print("* 디버깅 로그를 통해 작동 상태를 확인하세요.\n")
|
|
|
|
|
|
|
|
|
|
| 278 |
demo.launch(debug=True, show_error=True)
|
| 279 |
print("\n=== 웹 인터페이스 종료 ===")
|