이정수/품질관리팀 commited on
Commit
b7ede89
·
1 Parent(s): 79e1e8d
Files changed (1) hide show
  1. app.py +283 -189
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
- # 전역 변수로 서버 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
@@ -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: 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}"
37
- except RequestException as e:
38
- return f"서버 연결 실패: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
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:
49
- return f"상태 확인 실패: HTTP {response.status_code}\n{response.text}"
 
50
  except RequestException as e:
51
- return f"상태 확인 중 오류 발생: {str(e)}"
 
52
 
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:
64
- return f"에코 테스트 실패: HTTP {response.status_code}\n{response.text}"
 
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],
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("=== LocalPCAgent 웹 인터페이스 시작 ===\n")
276
- print("* UI 업데이트 로직 수정됨 (JavaScript 제거, Gradio 표준 방식 적용).")
277
- print("* 디버깅 로그를 통해 작동 상태를 확인하세요.\n")
 
 
 
 
 
 
278
  demo.launch(debug=True, show_error=True)
279
- print("\n=== 웹 인터페이스 종료 ===")
 
 
 
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("==============================================")