jeongsoo commited on
Commit
1c2d73c
ยท
1 Parent(s): 637ba57
.gitignore CHANGED
@@ -19,6 +19,7 @@ var/
19
  *.egg-info/
20
  .installed.cfg
21
  *.egg
 
22
 
23
  # Virtual Environment
24
  venv/
 
19
  *.egg-info/
20
  .installed.cfg
21
  *.egg
22
+ LocalPCAgent/
23
 
24
  # Virtual Environment
25
  venv/
app/app_device_routes.py CHANGED
@@ -562,5 +562,115 @@ def register_device_routes(app, login_required, DEVICE_SERVER_URL):
562
  logger.debug("์ž„์‹œ ID๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์•„ ์‚ญ์ œ ๊ฑด๋„ˆ<0xEB>๋œ€.")
563
  elif not get_device_url():
564
  logger.warning("์žฅ์น˜ ์„œ๋ฒ„ URL์ด ์—†์–ด ์ž„์‹œ ํ”„๋กœ๊ทธ๋žจ ์‚ญ์ œ ๊ฑด๋„ˆ<0xEB>๋œ€.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  # register_device_routes ํ•จ์ˆ˜์˜ ๋
 
562
  logger.debug("์ž„์‹œ ID๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์•„ ์‚ญ์ œ ๊ฑด๋„ˆ<0xEB>๋œ€.")
563
  elif not get_device_url():
564
  logger.warning("์žฅ์น˜ ์„œ๋ฒ„ URL์ด ์—†์–ด ์ž„์‹œ ํ”„๋กœ๊ทธ๋žจ ์‚ญ์ œ ๊ฑด๋„ˆ<0xEB>๋œ€.")
565
+ @app.route('/api/device/scan-ports', methods=['GET'])
566
+ @login_required
567
+ def scan_device_ports():
568
+ """๋กœ์ปฌPC์— ์—ฐ๊ฒฐ๋œ ์žฅ์น˜(COM ํฌํŠธ ๋ฐ USB ์žฅ์น˜) ๋ชฉ๋ก ์กฐํšŒ API"""
569
+ logger.info("์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์š”์ฒญ")
570
+
571
+ try:
572
+ # ํ”„๋กœ๊ทธ๋žจ ID (์žฅ์น˜ ์Šค์บ” ์Šคํฌ๋ฆฝํŠธ ID - scan_ports)
573
+ program_id = "scan_ports"
574
+
575
+ # ํ˜„์žฌ ์žฅ์น˜ ์„œ๋ฒ„ URL ๊ฐ€์ ธ์˜ค๊ธฐ
576
+ current_device_url = get_device_url()
577
+ if not current_device_url:
578
+ logger.error("์žฅ์น˜ ์„œ๋ฒ„ URL์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
579
+ return jsonify({
580
+ "success": False,
581
+ "error": "์žฅ์น˜ ์„œ๋ฒ„ URL์ด ์„ค์ •๋˜์ง€ ์•Š์•„ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
582
+ }), 503 # Service Unavailable
583
+
584
+ # ์žฅ์น˜ ์Šค์บ” ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ (ํ”„๋กœ๊ทธ๋žจ ID: scan_ports)
585
+ api_path = f"/api/programs/{program_id}/execute"
586
+ logger.info(f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ: {current_device_url}{api_path}")
587
+
588
+ # ์š”์ฒญ ์ „์†ก: ์žฅ์น˜ ์Šค์บ” ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰
589
+ response = requests.post(
590
+ f"{current_device_url}{api_path}",
591
+ json={}, # ํ•„์š”ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์—ฌ๊ธฐ์— ์ถ”๊ฐ€
592
+ timeout=20 # ์žฅ์น˜ ์Šค์บ”์€ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํƒ€์ž„์•„์›ƒ ์ฆ๊ฐ€
593
+ )
594
+ logger.debug(f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")
595
+
596
+ if response.status_code == 200:
597
+ try:
598
+ data = response.json()
599
+ # LocalPCAgent ์‘๋‹ต์—์„œ ํ•„์š”ํ•œ ์ •๋ณด ์ถ”์ถœ
600
+ if data.get("success", False):
601
+ # ์Šค์บ” ๊ฒฐ๊ณผ ์ถœ๋ ฅ ๊ฐ€์ ธ์˜ค๊ธฐ
602
+ output = data.get("output", "")
603
+
604
+ try:
605
+ # ์ถœ๋ ฅ์ด JSON ํ˜•์‹์ธ์ง€ ํ™•์ธ ๋ฐ ํŒŒ์‹ฑ
606
+ scan_results = json.loads(output)
607
+ logger.info("์žฅ์น˜ ํฌํŠธ ์Šค์บ” ๊ฒฐ๊ณผ ํŒŒ์‹ฑ ์„ฑ๊ณต")
608
+
609
+ # ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
610
+ return jsonify({
611
+ "success": True,
612
+ "timestamp": scan_results.get("timestamp", ""),
613
+ "system_info": scan_results.get("system_info", {}),
614
+ "devices": scan_results.get("devices", {})
615
+ })
616
+ except json.JSONDecodeError as json_err:
617
+ logger.error(f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ๊ฒฐ๊ณผ JSON ํŒŒ์‹ฑ ์‹คํŒจ: {json_err}")
618
+ return jsonify({
619
+ "success": False,
620
+ "error": "์žฅ์น˜ ํฌํŠธ ์Šค์บ” ๊ฒฐ๊ณผ๋ฅผ JSON์œผ๋กœ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",
621
+ "raw_output": output[:1000] # ๊ธด ์ถœ๋ ฅ์€ ์ž˜๋ผ์„œ ๋ฐ˜ํ™˜
622
+ }), 500
623
+ else:
624
+ # ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์Šค์บ” ์ž์ฒด๊ฐ€ ์‹คํŒจํ•œ ๊ฒฝ์šฐ
625
+ error_message = data.get("error", "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜")
626
+ logger.error(f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ํ”„๋กœ๊ทธ๋žจ ์˜ค๋ฅ˜: {error_message}")
627
+ return jsonify({
628
+ "success": False,
629
+ "error": f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ํ”„๋กœ๊ทธ๋žจ ์˜ค๋ฅ˜: {error_message}"
630
+ }), 500
631
+ except Exception as parse_err:
632
+ logger.error(f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์‘๋‹ต ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {parse_err}")
633
+ return jsonify({
634
+ "success": False,
635
+ "error": f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์‘๋‹ต ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {str(parse_err)}"
636
+ }), 500
637
+ else:
638
+ # API ์š”์ฒญ ์ž์ฒด๊ฐ€ ์‹คํŒจํ•œ ๊ฒฝ์šฐ
639
+ error_message = f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” API ์š”์ฒญ ์‹คํŒจ (์ƒํƒœ ์ฝ”๋“œ: {response.status_code})"
640
+ try:
641
+ error_data = response.json()
642
+ if 'error' in error_data:
643
+ error_message += f": {error_data['error']}"
644
+ except:
645
+ if response.text:
646
+ error_message += f": {response.text[:200]}"
647
+
648
+ logger.error(error_message)
649
+ return jsonify({
650
+ "success": False,
651
+ "error": error_message
652
+ }), response.status_code
653
 
654
+ except requests.exceptions.Timeout:
655
+ error_message = "์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์š”์ฒญ ์‹œ๊ฐ„ ์ดˆ๊ณผ"
656
+ logger.error(error_message)
657
+ return jsonify({
658
+ "success": False,
659
+ "error": error_message
660
+ }), 504 # Gateway Timeout
661
+
662
+ except requests.exceptions.ConnectionError:
663
+ error_message = f"์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ ({get_device_url()})"
664
+ logger.error(error_message)
665
+ return jsonify({
666
+ "success": False,
667
+ "error": "์žฅ์น˜ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”."
668
+ }), 502 # Bad Gateway
669
+
670
+ except Exception as e:
671
+ logger.error(f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ: {e}", exc_info=True)
672
+ return jsonify({
673
+ "success": False,
674
+ "error": f"์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"
675
+ }), 500 # Internal Server Error
676
  # register_device_routes ํ•จ์ˆ˜์˜ ๋
app/static/css/device-style.css CHANGED
@@ -84,7 +84,9 @@
84
  }
85
 
86
  /* ์žฅ์น˜ ๊ธฐ๋Šฅ ์ปจํ…Œ์ด๋„ˆ */
87
- .device-functions {
 
 
88
  background-color: var(--bg-color-secondary);
89
  border-radius: 8px;
90
  padding: 15px;
@@ -92,11 +94,13 @@
92
  display: none; /* ์ดˆ๊ธฐ์—๋Š” ์ˆจ๊น€ */
93
  }
94
 
95
- .device-functions.active {
96
- display: block;
 
97
  }
98
 
99
- .device-functions h3 {
 
100
  margin-top: 0;
101
  margin-bottom: 15px;
102
  color: var(--text-color-primary);
@@ -146,27 +150,14 @@
146
  }
147
 
148
  /* ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ปจํ…Œ์ด๋„ˆ */
149
- .program-control {
150
- background-color: var(--bg-color-secondary);
151
- border-radius: 8px;
152
- padding: 15px;
153
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
154
- display: none; /* ์ดˆ๊ธฐ์—๋Š” ์ˆจ๊น€ */
155
- }
156
-
157
- .program-control.active {
158
- display: block;
159
- }
160
-
161
- .program-control h3 {
162
- margin-top: 0;
163
- margin-bottom: 15px;
164
- color: var(--text-color-primary);
165
- font-size: 1.2rem;
166
- }
167
-
168
  .program-list-container {
169
  margin-bottom: 20px;
 
 
 
 
 
 
170
  }
171
 
172
  .program-list {
@@ -179,6 +170,7 @@
179
  padding: 10px;
180
  text-align: left;
181
  border-bottom: 1px solid var(--border-color);
 
182
  }
183
 
184
  .program-list th {
@@ -230,6 +222,7 @@
230
  border-radius: 4px;
231
  margin-top: 10px;
232
  font-size: 0.9rem;
 
233
  }
234
 
235
  .execute-result.success {
@@ -250,6 +243,62 @@
250
  border: 1px solid rgba(255, 193, 7, 0.2);
251
  }
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  /* ๋กœ๋”ฉ ํ‘œ์‹œ */
254
  .loading-spinner {
255
  display: inline-block;
@@ -307,10 +356,121 @@
307
  padding: 8px 12px;
308
  cursor: pointer;
309
  font-size: 0.9rem;
310
- margin-top: 10px;
 
311
  transition: background-color 0.2s;
312
  }
313
 
314
  .retry-button:hover {
315
  background-color: var(--secondary-color-dark);
316
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
85
 
86
  /* ์žฅ์น˜ ๊ธฐ๋Šฅ ์ปจํ…Œ์ด๋„ˆ */
87
+ /* ์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ›„ ํ™œ์„ฑํ™”๋˜๋Š” ์„น์…˜๋“ค์˜ ๊ธฐ๋ณธ ์Šคํƒ€์ผ */
88
+ .device-functions,
89
+ .program-control {
90
  background-color: var(--bg-color-secondary);
91
  border-radius: 8px;
92
  padding: 15px;
 
94
  display: none; /* ์ดˆ๊ธฐ์—๋Š” ์ˆจ๊น€ */
95
  }
96
 
97
+ .device-functions.active,
98
+ .program-control.active {
99
+ display: block; /* ํ™œ์„ฑํ™” ์‹œ ๋ณด์ž„ */
100
  }
101
 
102
+ .device-functions h3,
103
+ .program-control h3 {
104
  margin-top: 0;
105
  margin-bottom: 15px;
106
  color: var(--text-color-primary);
 
150
  }
151
 
152
  /* ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ปจํ…Œ์ด๋„ˆ */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  .program-list-container {
154
  margin-bottom: 20px;
155
+ /* ์Šคํฌ๋กค์ด ํ•„์š”ํ•  ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ */
156
+ max-height: 300px;
157
+ overflow-y: auto;
158
+ border: 1px solid var(--border-color);
159
+ border-radius: 4px;
160
+ padding: 10px;
161
  }
162
 
163
  .program-list {
 
170
  padding: 10px;
171
  text-align: left;
172
  border-bottom: 1px solid var(--border-color);
173
+ font-size: 0.9rem;
174
  }
175
 
176
  .program-list th {
 
222
  border-radius: 4px;
223
  margin-top: 10px;
224
  font-size: 0.9rem;
225
+ min-height: 20px; /* ๊ฒฐ๊ณผ ์—†์„ ๋•Œ๋„ ์ตœ์†Œ ๋†’์ด ์œ ์ง€ */
226
  }
227
 
228
  .execute-result.success {
 
243
  border: 1px solid rgba(255, 193, 7, 0.2);
244
  }
245
 
246
+ /* ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ */
247
+ .custom-command-container {
248
+ display: flex;
249
+ gap: 10px;
250
+ margin-bottom: 15px;
251
+ }
252
+
253
+ .custom-command-container input {
254
+ flex: 1;
255
+ padding: 10px;
256
+ border: 1px solid var(--border-color);
257
+ border-radius: 4px;
258
+ font-size: 0.95rem;
259
+ }
260
+
261
+ .custom-command-container button {
262
+ background-color: var(--primary-color);
263
+ color: white;
264
+ border: none;
265
+ border-radius: 4px;
266
+ padding: 10px 15px;
267
+ cursor: pointer;
268
+ font-weight: 500;
269
+ transition: background-color 0.2s;
270
+ }
271
+
272
+ .custom-command-container button:hover {
273
+ background-color: var(--primary-color-dark);
274
+ }
275
+
276
+ .custom-command-container button:disabled {
277
+ background-color: var(--disabled-color);
278
+ cursor: not-allowed;
279
+ }
280
+
281
+ /* ๋ช…๋ น์–ด ์‹คํ–‰ ๊ฒฐ๊ณผ ์Šคํƒ€์ผ */
282
+ .command-output, .command-error {
283
+ margin-top: 10px;
284
+ padding: 10px;
285
+ background-color: var(--bg-color-tertiary);
286
+ border-radius: 4px;
287
+ border: 1px solid var(--border-color);
288
+ }
289
+
290
+ .command-output pre, .command-error pre {
291
+ white-space: pre-wrap;
292
+ word-wrap: break-word;
293
+ font-size: 0.85rem;
294
+ margin: 0;
295
+ }
296
+
297
+ .command-error pre {
298
+ color: #dc3545; /* ์˜ค๋ฅ˜ ํ…์ŠคํŠธ ์ƒ‰์ƒ */
299
+ }
300
+
301
+
302
  /* ๋กœ๋”ฉ ํ‘œ์‹œ */
303
  .loading-spinner {
304
  display: inline-block;
 
356
  padding: 8px 12px;
357
  cursor: pointer;
358
  font-size: 0.9rem;
359
+ margin-top: 10px; /* ํ•„์š”์‹œ ์กฐ์ • */
360
+ margin-left: 10px; /* ์•ž ์š”์†Œ์™€ ๊ฐ„๊ฒฉ */
361
  transition: background-color 0.2s;
362
  }
363
 
364
  .retry-button:hover {
365
  background-color: var(--secondary-color-dark);
366
  }
367
+
368
+ /* ================== ์žฅ์น˜ ํฌํŠธ ์กฐํšŒ ์Šคํƒ€์ผ ์ถ”๊ฐ€ ================== */
369
+ .ports-status {
370
+ margin: 10px 0;
371
+ padding: 10px;
372
+ border-radius: 5px;
373
+ min-height: 20px;
374
+ font-size: 0.9rem; /* ๋‹ค๋ฅธ ์ƒํƒœ ๋ฉ”์‹œ์ง€์™€ ์ผ๊ด€์„ฑ */
375
+ }
376
+
377
+ .ports-status.success {
378
+ background-color: rgba(0, 200, 83, 0.1); /* ๊ธฐ์กด .success ์Šคํƒ€์ผ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ */
379
+ color: #00c853; /* ์ข€ ๋” ๋ฐ์€ ๋…น์ƒ‰ */
380
+ border: 1px solid #00c853;
381
+ }
382
+
383
+ .ports-status.error {
384
+ background-color: rgba(244, 67, 54, 0.1); /* ๊ธฐ์กด .error ์Šคํƒ€์ผ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ */
385
+ color: #f44336; /* ์ข€ ๋” ๋ฐ์€ ๋นจ๊ฐ• */
386
+ border: 1px solid #f44336;
387
+ }
388
+
389
+ .ports-status.warning {
390
+ background-color: rgba(255, 152, 0, 0.1); /* ๊ธฐ์กด .warning ์Šคํƒ€์ผ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ */
391
+ color: #ff9800; /* ์ข€ ๋” ๋ฐ์€ ์ฃผํ™ฉ */
392
+ border: 1px solid #ff9800;
393
+ }
394
+
395
+ .ports-results {
396
+ margin-top: 15px;
397
+ border: 1px solid var(--border-color); /* ๋ณ€์ˆ˜ ์‚ฌ์šฉ */
398
+ border-radius: 5px;
399
+ padding: 0; /* ๋‚ด๋ถ€ ์„น์…˜ ํŒจ๋”ฉ์œผ๋กœ ์กฐ์ • */
400
+ max-height: 500px; /* ์ตœ๋Œ€ ๋†’์ด ์ œํ•œ */
401
+ overflow-y: auto; /* ๋‚ด์šฉ ๋งŽ์œผ๋ฉด ์Šคํฌ๋กค */
402
+ background-color: var(--bg-color-tertiary); /* ์•ฝ๊ฐ„์˜ ๋ฐฐ๊ฒฝ์ƒ‰ */
403
+ }
404
+
405
+ /* ํฌํŠธ ๊ฒฐ๊ณผ ์ปจํ…Œ์ด๋„ˆ (๋‚ด๋ถ€ ํŒจ๋”ฉ์šฉ) */
406
+ .ports-results-container {
407
+ padding: 15px;
408
+ }
409
+
410
+ .ports-section {
411
+ margin-bottom: 20px;
412
+ }
413
+ /* ๋งˆ์ง€๋ง‰ ์„น์…˜์˜ ํ•˜๋‹จ ๋งˆ์ง„ ์ œ๊ฑฐ */
414
+ .ports-section:last-child {
415
+ margin-bottom: 0;
416
+ }
417
+
418
+
419
+ .ports-section h4 {
420
+ margin-top: 0;
421
+ margin-bottom: 10px;
422
+ font-size: 1rem; /* ๋‹ค๋ฅธ h3 ์™€ ์œ ์‚ฌํ•˜๊ฒŒ */
423
+ color: var(--text-color-primary); /* ๋ณ€์ˆ˜ ์‚ฌ์šฉ */
424
+ border-bottom: 1px solid var(--border-color); /* ๋ณ€์ˆ˜ ์‚ฌ์šฉ */
425
+ padding-bottom: 5px;
426
+ }
427
+
428
+ .info-row {
429
+ display: flex;
430
+ margin-bottom: 5px;
431
+ font-size: 0.9rem; /* ๊ธ€์ž ํฌ๊ธฐ ํ†ต์ผ */
432
+ }
433
+
434
+ .info-label {
435
+ font-weight: bold;
436
+ width: 100px;
437
+ flex-shrink: 0; /* ๋ผ๋ฒจ ๋„ˆ๋น„ ๊ณ ์ • */
438
+ color: var(--text-color-secondary); /* ์•ฝ๊ฐ„ ์—ฐํ•œ ์ƒ‰ */
439
+ }
440
+
441
+ .info-value {
442
+ color: var(--text-color-primary);
443
+ }
444
+
445
+ .ports-table {
446
+ width: 100%;
447
+ border-collapse: collapse;
448
+ margin-top: 10px;
449
+ font-size: 0.85rem; /* ํ…Œ์ด๋ธ” ๊ธ€์ž ์•ฝ๊ฐ„ ์ž‘๊ฒŒ */
450
+ }
451
+
452
+ .ports-table th,
453
+ .ports-table td {
454
+ padding: 8px 10px; /* ํŒจ๋”ฉ ์กฐ์ • */
455
+ text-align: left;
456
+ border-bottom: 1px solid var(--border-color-lighter, #eee); /* ๋” ์—ฐํ•œ ๊ตฌ๋ถ„์„ , ๋ณ€์ˆ˜ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ */
457
+ vertical-align: top; /* ์—ฌ๋Ÿฌ ์ค„์ผ ๊ฒฝ์šฐ ์œ„ ์ •๋ ฌ */
458
+ }
459
+
460
+ .ports-table th {
461
+ background-color: var(--bg-color-secondary); /* ์•ฝ๊ฐ„ ๋‹ค๋ฅธ ๋ฐฐ๊ฒฝ */
462
+ font-weight: 500; /* ๋ณดํ†ต ๋‘๊ป˜ */
463
+ color: var(--text-color-primary);
464
+ }
465
+
466
+ .ports-table tbody tr:hover {
467
+ background-color: var(--bg-color-hover, #f9f9f9); /* ๋ณ€์ˆ˜ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ */
468
+ }
469
+
470
+ .no-ports-message {
471
+ color: var(--text-color-secondary); /* ๋ณ€์ˆ˜ ์‚ฌ์šฉ */
472
+ font-style: italic;
473
+ padding: 10px 0;
474
+ font-size: 0.9rem;
475
+ }
476
+ /* ================== ์ถ”๊ฐ€ ๋ ================================= */
app/static/js/app-device.js CHANGED
@@ -1,5 +1,5 @@
1
  /**
2
- * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์žฅ์น˜ ์ œ์–ด JavaScript
3
  */
4
 
5
  // ์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ
@@ -27,32 +27,32 @@ const DeviceControl = {
27
  deviceStatusResult: null,
28
 
29
  // ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ (๋ฏธ๋ฆฌ ์ •์˜๋œ)
30
- deviceProgramControl: null, // ์ด ์š”์†Œ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ ํ•„์š”
31
  getProgramsBtn: null,
32
  programsList: null,
33
  programSelectDropdown: null,
34
  executeProgramBtn: null,
35
  executeResult: null,
36
 
37
- // ================== ์ถ”๊ฐ€ ์‹œ์ž‘ 1/4 ==================
38
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
39
  deviceCustomControl: null,
40
  customCommandInput: null,
41
  executeCustomBtn: null,
42
- customExecuteResult: null
43
- // ================== ์ถ”๊ฐ€ ๋ 1/4 ====================
 
 
 
 
 
 
44
  },
45
 
46
  // ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
47
  init: function() {
48
  console.log('์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™” ์ค‘...');
49
-
50
- // DOM ์š”์†Œ ์ฐธ์กฐ ๊ฐ€์ ธ์˜ค๊ธฐ
51
  this.initElements();
52
-
53
- // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
54
  this.initEventListeners();
55
-
56
  console.log('์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
57
  },
58
 
@@ -80,13 +80,18 @@ const DeviceControl = {
80
  this.elements.executeProgramBtn = document.getElementById('executeProgramBtn');
81
  this.elements.executeResult = document.getElementById('executeResult');
82
 
83
- // ================== ์ถ”๊ฐ€ ์‹œ์ž‘ 2/4 ==================
84
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
85
  this.elements.deviceCustomControl = document.getElementById('deviceCustomControl');
86
  this.elements.customCommandInput = document.getElementById('customCommandInput');
87
  this.elements.executeCustomBtn = document.getElementById('executeCustomBtn');
88
  this.elements.customExecuteResult = document.getElementById('customExecuteResult');
89
- // ================== ์ถ”๊ฐ€ ๋ 2/4 ====================
 
 
 
 
 
 
90
 
91
  console.log('์žฅ์น˜ ์ œ์–ด DOM ์š”์†Œ ์ฐธ์กฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
92
  },
@@ -153,7 +158,6 @@ const DeviceControl = {
153
  });
154
  }
155
 
156
- // ================== ์ถ”๊ฐ€ ์‹œ์ž‘ 3/4 ==================
157
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ๋ฒ„ํŠผ ํด๋ฆญ
158
  if (this.elements.executeCustomBtn) {
159
  this.elements.executeCustomBtn.addEventListener('click', () => {
@@ -174,7 +178,15 @@ const DeviceControl = {
174
  }
175
  });
176
  }
177
- // ================== ์ถ”๊ฐ€ ๋ 3/4 ====================
 
 
 
 
 
 
 
 
178
 
179
  console.log('์žฅ์น˜ ์ œ์–ด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก ์™„๋ฃŒ');
180
  },
@@ -189,8 +201,8 @@ const DeviceControl = {
189
  tabContents.forEach(content => content.classList.remove('active'));
190
 
191
  // ์žฅ์น˜ ์ œ์–ด ํƒญ ํ™œ์„ฑํ™”
192
- this.elements.deviceTab.classList.add('active');
193
- this.elements.deviceSection.classList.add('active');
194
 
195
  console.log('์žฅ์น˜ ์ œ์–ด ํƒญ์œผ๋กœ ์ „ํ™˜ ์™„๋ฃŒ');
196
  },
@@ -201,7 +213,7 @@ const DeviceControl = {
201
  const inputUrl = this.elements.deviceServerUrlInput.value.trim();
202
 
203
  // ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘ UI ์—…๋ฐ์ดํŠธ
204
- this.elements.connectDeviceServerBtn.disabled = true;
205
  this.updateConnectionStatus('connecting', 'ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์ €์žฅ๋œ ์„œ๋ฒ„๋กœ ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘...');
206
 
207
  try {
@@ -215,147 +227,49 @@ const DeviceControl = {
215
  const data = await response.json();
216
 
217
  if (response.ok && data.success) {
218
- // ์—ฐ๊ฒฐ ์„ฑ๊ณต
219
  console.log('ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต:', data);
220
  this.isConnected = true;
221
- this.updateConnectionStatus('connected', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! ์ƒํƒœ: ${data.server_status || '์ •์ƒ'}`);
 
 
222
 
223
  // ๊ธฐ๋Šฅ UI ํ™œ์„ฑํ™”
224
  if(this.elements.deviceBasicFunctions) this.elements.deviceBasicFunctions.classList.add('active');
225
  if(this.elements.deviceProgramControl) this.elements.deviceProgramControl.classList.add('active');
226
- // ================== ์ถ”๊ฐ€ (connectServer ์„ฑ๊ณต ์‹œ UI ํ™œ์„ฑํ™”) ==================
227
  if(this.elements.deviceCustomControl) this.elements.deviceCustomControl.classList.add('active');
228
- // ================== ์ถ”๊ฐ€ ๋ =============================================
 
 
229
 
230
  // ์žฅ์น˜ ์ƒํƒœ ์ž๋™ ์ฒดํฌ
231
  this.checkDeviceStatus();
232
-
233
  // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ž๋™ ๋กœ๋“œ
234
  this.loadProgramsList();
235
-
236
  // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
237
  AppUtils.addSystemNotification(`์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! (ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL)`);
 
238
  } else {
239
  // ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹คํŒจ, ์ž…๋ ฅ๋œ URL๋กœ ์‹œ๋„
240
  console.warn('ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ, ์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค:', data);
241
-
242
- // ์ž…๋ ฅ URL์ด ์žˆ๋Š”์ง€ ํ™•์ธ
243
  if (!inputUrl) {
244
  console.error('์ž…๋ ฅ๋œ URL์ด ์—†์–ด ์—ฐ๊ฒฐ ์‹คํŒจ');
245
  this.isConnected = false;
246
  this.updateConnectionStatus('error', 'ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹คํŒจ ๋ฐ ์ž…๋ ฅ๋œ URL์ด ์—†์Šต๋‹ˆ๋‹ค. URL์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
247
- return;
248
  }
249
-
250
  // ์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„
251
- this.updateConnectionStatus('connecting', `์ž…๋ ฅ URL(${inputUrl})๋กœ ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘...`);
252
- console.log(`์ž…๋ ฅํ•œ URL๋กœ ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๋„: ${inputUrl}`);
253
-
254
- // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ - ์ž…๋ ฅ URL ์‚ฌ์šฉ
255
- const customUrlResponse = await AppUtils.fetchWithTimeout('/api/device/connect', {
256
- method: 'POST',
257
- headers: {
258
- 'Content-Type': 'application/json'
259
- },
260
- body: JSON.stringify({ url: inputUrl })
261
- }, 10000);
262
-
263
- const customUrlData = await customUrlResponse.json();
264
-
265
- if (customUrlResponse.ok && customUrlData.success) {
266
- // ์ž…๋ ฅ URL ์—ฐ๊ฒฐ ์„ฑ๊ณต
267
- console.log('์ž…๋ ฅ URL ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต:', customUrlData);
268
- this.isConnected = true;
269
- this.updateConnectionStatus('connected', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! ์ƒํƒœ: ${customUrlData.server_status || '์ •์ƒ'}`);
270
-
271
- // ๊ธฐ๋Šฅ UI ํ™œ์„ฑํ™”
272
- if(this.elements.deviceBasicFunctions) this.elements.deviceBasicFunctions.classList.add('active');
273
- if(this.elements.deviceProgramControl) this.elements.deviceProgramControl.classList.add('active');
274
- // ================== ์ถ”๊ฐ€ (connectServer ์„ฑ๊ณต ์‹œ UI ํ™œ์„ฑํ™”) ==================
275
- if(this.elements.deviceCustomControl) this.elements.deviceCustomControl.classList.add('active');
276
- // ================== ์ถ”๊ฐ€ ๋ =============================================
277
-
278
-
279
- // ์žฅ์น˜ ์ƒํƒœ ์ž๋™ ์ฒดํฌ
280
- this.checkDeviceStatus();
281
-
282
- // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ž๋™ ๋กœ๋“œ
283
- this.loadProgramsList();
284
-
285
- // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
286
- AppUtils.addSystemNotification(`์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! (${inputUrl})`);
287
- } else {
288
- // ์ž…๋ ฅ URL ์—ฐ๊ฒฐ ์‹คํŒจ
289
- console.error('์ž…๋ ฅ URL ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ:', customUrlData);
290
- this.isConnected = false;
291
- this.updateConnectionStatus('error', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ: ${customUrlData.error || '์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜'}`);
292
- }
293
  }
294
  } catch (error) {
295
- // ์˜ˆ์™ธ ๋ฐœ์ƒ
296
- console.error('์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
297
  this.isConnected = false;
298
-
299
- // ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹คํŒจ, ์ž…๋ ฅ๋œ URL๋กœ ์‹œ๋„
300
  if (inputUrl) {
301
  console.warn('ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ, ์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค');
302
-
303
- try {
304
- // ์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„
305
- this.updateConnectionStatus('connecting', `์ž…๋ ฅ URL(${inputUrl})๋กœ ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘...`);
306
- console.log(`์ž…๋ ฅํ•œ URL๋กœ ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๋„: ${inputUrl}`);
307
-
308
- // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ - ์ž…๋ ฅ URL ์‚ฌ์šฉ
309
- const customUrlResponse = await AppUtils.fetchWithTimeout('/api/device/connect', {
310
- method: 'POST',
311
- headers: {
312
- 'Content-Type': 'application/json'
313
- },
314
- body: JSON.stringify({ url: inputUrl })
315
- }, 10000);
316
-
317
- const customUrlData = await customUrlResponse.json();
318
-
319
- if (customUrlResponse.ok && customUrlData.success) {
320
- // ์ž…๋ ฅ URL ์—ฐ๊ฒฐ ์„ฑ๊ณต
321
- console.log('์ž…๋ ฅ URL ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต:', customUrlData);
322
- this.isConnected = true;
323
- this.updateConnectionStatus('connected', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! ์ƒํƒœ: ${customUrlData.server_status || '์ •์ƒ'}`);
324
-
325
- // ๊ธฐ๋Šฅ UI ํ™œ์„ฑํ™”
326
- if(this.elements.deviceBasicFunctions) this.elements.deviceBasicFunctions.classList.add('active');
327
- if(this.elements.deviceProgramControl) this.elements.deviceProgramControl.classList.add('active');
328
- // ================== ์ถ”๊ฐ€ (connectServer ์„ฑ๊ณต ์‹œ UI ํ™œ์„ฑํ™”) ==================
329
- if(this.elements.deviceCustomControl) this.elements.deviceCustomControl.classList.add('active');
330
- // ================== ์ถ”๊ฐ€ ๋ =============================================
331
-
332
-
333
- // ์žฅ์น˜ ์ƒํƒœ ์ž๋™ ์ฒดํฌ
334
- this.checkDeviceStatus();
335
-
336
- // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ž๋™ ๋กœ๋“œ
337
- this.loadProgramsList();
338
-
339
- // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
340
- AppUtils.addSystemNotification(`์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! (${inputUrl})`);
341
- return; // ์„ฑ๊ณตํ•˜๋ฉด ์—ฌ๊ธฐ์„œ ์ข…๋ฃŒ
342
- } else {
343
- // ์ž…๋ ฅ URL ์—ฐ๊ฒฐ ์‹คํŒจ
344
- console.error('์ž…๋ ฅ URL ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ:', customUrlData);
345
- this.updateConnectionStatus('error', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ: ${customUrlData.error || '์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜'}`);
346
- }
347
- } catch (inputUrlError) {
348
- // ์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜
349
- console.error('์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', inputUrlError);
350
-
351
- if (inputUrlError.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
352
- this.updateConnectionStatus('error', '์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.');
353
- } else {
354
- this.updateConnectionStatus('error', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜: ${inputUrlError.message}`);
355
- }
356
- }
357
  } else {
358
- // ํ…์ŠคํŠธ๋ฐ•์Šค์— URL์ด ์—†๋Š” ๊ฒฝ์šฐ
359
  if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
360
  this.updateConnectionStatus('error', 'ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ. URL์„ ์ž…๋ ฅํ•˜์—ฌ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
361
  } else {
@@ -364,10 +278,63 @@ const DeviceControl = {
364
  }
365
  } finally {
366
  // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
367
- this.elements.connectDeviceServerBtn.disabled = false;
368
  }
369
  },
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  // ์—ฐ๊ฒฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
372
  updateConnectionStatus: function(status, message) {
373
  const statusElement = this.elements.deviceConnectionStatus;
@@ -375,56 +342,44 @@ const DeviceControl = {
375
 
376
  // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
377
  statusElement.classList.remove('connected', 'disconnected', 'error', 'connecting');
378
-
379
  // ์ƒํƒœ์— ๋”ฐ๋ผ ํด๋ž˜์Šค ์ถ”๊ฐ€
380
  statusElement.classList.add(status);
381
-
382
  // ๋ฉ”์‹œ์ง€ ์—…๋ฐ์ดํŠธ
383
  statusElement.textContent = message;
384
-
385
  console.log(`์—ฐ๊ฒฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ: ${status} - ${message}`);
386
  },
387
 
388
  // ์žฅ์น˜ ์ƒํƒœ ํ™•์ธ
389
  checkDeviceStatus: async function() {
390
  if (!this.isConnected) {
391
- this.elements.deviceStatusResult.value = '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.';
392
  console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
393
  return;
394
  }
395
-
396
  // ์ƒํƒœ ํ™•์ธ ์ค‘ UI ์—…๋ฐ์ดํŠธ
397
- this.elements.checkDeviceStatusBtn.disabled = true;
398
- this.elements.deviceStatusResult.value = '์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘...';
399
 
400
  try {
401
  console.log('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ ์ „์†ก');
402
-
403
  // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
404
- const response = await AppUtils.fetchWithTimeout('/api/device/status', {
405
- method: 'GET'
406
- });
407
-
408
  const data = await response.json();
409
 
410
  if (response.ok && data.success) {
411
- // ์ƒํƒœ ํ™•์ธ ์„ฑ๊ณต
412
  console.log('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์„ฑ๊ณต:', data);
413
  this.isStatusChecked = true;
414
  // JSON ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๊ธฐ ์ข‹๊ฒŒ ํฌ๋งทํŒ…ํ•˜์—ฌ ํ‘œ์‹œ
415
- this.elements.deviceStatusResult.value = JSON.stringify(data.data || data, null, 2); // data.data ์šฐ์„  ํ™•์ธ
416
  } else {
417
- // ์ƒํƒœ ํ™•์ธ ์‹คํŒจ
418
  console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์‹คํŒจ:', data);
419
- this.elements.deviceStatusResult.value = `์ƒํƒœ ํ™•์ธ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`;
420
  }
421
  } catch (error) {
422
- // ์˜ˆ์™ธ ๋ฐœ์ƒ
423
  console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
424
- this.elements.deviceStatusResult.value = `์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`;
425
  } finally {
426
- // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
427
- this.elements.checkDeviceStatusBtn.disabled = false;
428
  }
429
  },
430
 
@@ -435,8 +390,6 @@ const DeviceControl = {
435
  console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
436
  return;
437
  }
438
-
439
- // ์ด๋ฏธ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€
440
  if (this.isLoadingPrograms) {
441
  console.log('์ด๋ฏธ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋”ฉ ์ค‘');
442
  return;
@@ -444,131 +397,76 @@ const DeviceControl = {
444
 
445
  // ๋กœ๋”ฉ ์ค‘ UI ์—…๋ฐ์ดํŠธ
446
  this.isLoadingPrograms = true;
447
- if(this.elements.getProgramsBtn) this.elements.getProgramsBtn.disabled = true; // ๋ฒ„ํŠผ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
448
- if(this.elements.programsList) { // ๋ชฉ๋ก ์š”์†Œ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
449
- this.elements.programsList.innerHTML = `
450
- <div class="loading-message">
451
- ${AppUtils.createLoadingSpinner()} ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์ค‘...
452
- </div>
453
- `;
454
  }
455
 
456
-
457
  try {
458
  console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์š”์ฒญ ์ „์†ก');
459
-
460
  // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
461
- const response = await AppUtils.fetchWithTimeout('/api/device/programs', {
462
- method: 'GET'
463
- });
464
-
465
  const data = await response.json();
466
 
467
  if (response.ok && data.success) {
468
- // ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต
469
  console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต:', data);
470
  this.programsList = data.programs || [];
471
-
472
- // ๋ชฉ๋ก ํ‘œ์‹œ
473
  this.displayProgramsList();
474
-
475
- // ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
476
  this.updateProgramsDropdown();
477
-
478
- // ์‹คํ–‰ ๋ฒ„ํŠผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
479
  this.updateExecuteButton();
480
  } else {
481
- // ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ
482
  console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:', data);
483
  this.showProgramsError(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`);
484
  }
485
  } catch (error) {
486
- // ์˜ˆ์™ธ ๋ฐœ์ƒ
487
  console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
488
  this.showProgramsError(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
489
  } finally {
490
- // ๋กœ๋”ฉ ์ƒํƒœ ๋ฐ ๋ฒ„ํŠผ ์ƒํƒœ ๋ณต์›
491
  this.isLoadingPrograms = false;
492
- if(this.elements.getProgramsBtn) this.elements.getProgramsBtn.disabled = false; // ๋ฒ„ํŠผ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
493
  }
494
  },
495
 
496
  // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
497
  displayProgramsList: function() {
498
  const programsListElement = this.elements.programsList;
499
- if (!programsListElement) return; // ์š”์†Œ ์—†์œผ๋ฉด ์ข…๋ฃŒ
500
 
501
  if (!this.programsList || this.programsList.length === 0) {
502
- programsListElement.innerHTML = `
503
- <div class="no-programs-message">
504
- <i class="fas fa-info-circle"></i> ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ์ด ์—†์Šต๋‹ˆ๋‹ค.
505
- </div>
506
- `;
507
  return;
508
  }
509
-
510
  // ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
511
- let html = `
512
- <table class="program-list">
513
- <thead>
514
- <tr>
515
- <th>์ด๋ฆ„</th>
516
- <th>์„ค๋ช…</th>
517
- <th>๊ฒฝ๋กœ</th>
518
- </tr>
519
- </thead>
520
- <tbody>
521
- `;
522
-
523
- // ํ”„๋กœ๊ทธ๋žจ ํ•ญ๋ชฉ ์ƒ์„ฑ
524
  this.programsList.forEach(program => {
525
- html += `
526
- <tr>
527
- <td>${AppUtils.escapeHtml(program.name || '์•Œ ์ˆ˜ ์—†์Œ')}</td>
528
- <td>${AppUtils.escapeHtml(program.description || '-')}</td>
529
- <td>${AppUtils.escapeHtml(program.path || '-')}</td>
530
- </tr>
531
- `;
532
  });
533
-
534
- html += `
535
- </tbody>
536
- </table>
537
- <div style="margin-top: 10px; font-size: 0.9em; color: #666;">
538
- ์ด ${this.programsList.length}๊ฐœ ํ”„๋กœ๊ทธ๋žจ
539
- </div>
540
- `;
541
-
542
  programsListElement.innerHTML = html;
543
  },
544
 
545
  // ํ”„๋กœ๊ทธ๋žจ ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
546
  updateProgramsDropdown: function() {
547
  const dropdown = this.elements.programSelectDropdown;
548
- if (!dropdown) return; // ์š”์†Œ ์—†์œผ๋ฉด ์ข…๋ฃŒ
549
-
550
- // ๊ธฐ์กด ์˜ต์…˜ ์ œ๊ฑฐ
551
- dropdown.innerHTML = '';
552
 
553
- // ๊ธฐ๋ณธ ์˜ต์…˜ ์ถ”๊ฐ€
554
  const defaultOption = document.createElement('option');
555
  defaultOption.value = '';
556
- defaultOption.textContent = this.programsList.length > 0
557
- ? '-- ์‹คํ–‰ํ•  ํ”„๋กœ๊ทธ๋žจ ์„ ํƒ --'
558
- : '-- ํ”„๋กœ๊ทธ๋žจ ์—†์Œ --';
559
  dropdown.appendChild(defaultOption);
560
 
561
- // ํ”„๋กœ๊ทธ๋žจ ์˜ต์…˜ ์ถ”๊ฐ€
562
  this.programsList.forEach(program => {
563
  const option = document.createElement('option');
564
  option.value = program.id || '';
565
  option.textContent = program.name || '์•Œ ์ˆ˜ ์—†์Œ';
566
-
567
- // ์„ค๋ช…์ด ์žˆ์œผ๋ฉด ๊ด„ํ˜ธ๋กœ ์ถ”๊ฐ€
568
  if (program.description) {
569
  option.textContent += ` (${program.description})`;
570
  }
571
-
572
  dropdown.appendChild(option);
573
  });
574
  },
@@ -577,23 +475,18 @@ const DeviceControl = {
577
  updateExecuteButton: function() {
578
  const dropdown = this.elements.programSelectDropdown;
579
  const executeBtn = this.elements.executeProgramBtn;
580
- if (!dropdown || !executeBtn) return; // ์š”์†Œ ์—†์œผ๋ฉด ์ข…๋ฃŒ
581
-
582
- // ์„ ํƒ๋œ ํ”„๋กœ๊ทธ๋žจ์ด ์žˆ์„ ๋•Œ๋งŒ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
583
- executeBtn.disabled = !dropdown.value;
584
  },
585
 
586
  // ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ (๋ฏธ๋ฆฌ ์ •์˜๋œ)
587
  executeProgram: async function(programId) {
588
  if (!this.isConnected) {
589
  this.showExecuteResult('error', '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
590
- console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
591
  return;
592
  }
593
-
594
  if (!programId) {
595
  this.showExecuteResult('error', '์˜ค๋ฅ˜: ์‹คํ–‰ํ•  ํ”„๋กœ๊ทธ๋žจ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.');
596
- console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ํ”„๋กœ๊ทธ๋žจ ID ์—†์Œ');
597
  return;
598
  }
599
 
@@ -603,49 +496,38 @@ const DeviceControl = {
603
 
604
  try {
605
  console.log(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์ „์†ก: ${programId}`);
606
-
607
  // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
608
  const response = await AppUtils.fetchWithTimeout(`/api/device/programs/${programId}/execute`, {
609
  method: 'POST',
610
- headers: {
611
- 'Content-Type': 'application/json'
612
- },
613
  body: JSON.stringify({})
614
- }, 15000); // 15์ดˆ ํƒ€์ž„์•„์›ƒ (์‹คํ–‰์— ์‹œ๊ฐ„์ด ๋” ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Œ)
615
 
616
  const data = await response.json();
617
-
618
  if (response.ok && data.success) {
619
- // ์‹คํ–‰ ์„ฑ๊ณต
620
  console.log('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต:', data);
621
  this.showExecuteResult('success', `์‹คํ–‰ ์„ฑ๊ณต: ${data.message || 'ํ”„๋กœ๊ทธ๋žจ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'}`);
622
-
623
- // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
624
  AppUtils.addSystemNotification(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต: ${this.getSelectedProgramName()}`);
625
  } else {
626
- // ์‹คํ–‰ ์‹คํŒจ
627
  console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹คํŒจ:', data);
628
  this.showExecuteResult('error', `์‹คํ–‰ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`);
629
  }
630
  } catch (error) {
631
- // ์˜ˆ์™ธ ๋ฐœ์ƒ
632
  console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
633
-
634
  if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
635
  this.showExecuteResult('error', 'ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„ ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.');
636
  } else {
637
  this.showExecuteResult('error', `ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
638
  }
639
  } finally {
640
- // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
641
- if(this.elements.executeProgramBtn) this.elements.executeProgramBtn.disabled = false;
642
  }
643
  },
644
 
645
  // ์„ ํƒ๋œ ํ”„๋กœ๊ทธ๋žจ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ
646
  getSelectedProgramName: function() {
647
  const dropdown = this.elements.programSelectDropdown;
648
- if (!dropdown) return '์•Œ ์ˆ˜ ์—†๋Š” ํ”„๋กœ๊ทธ๋žจ';
649
  const selectedOption = dropdown.options[dropdown.selectedIndex];
650
  return selectedOption ? selectedOption.textContent : '์•Œ ์ˆ˜ ์—†๋Š” ํ”„๋กœ๊ทธ๋žจ';
651
  },
@@ -653,42 +535,28 @@ const DeviceControl = {
653
  // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์˜ค๋ฅ˜ ํ‘œ์‹œ
654
  showProgramsError: function(errorMessage) {
655
  const programsListElement = this.elements.programsList;
656
- if (!programsListElement) return; // ์š”์†Œ ์—†์œผ๋ฉด ์ข…๋ฃŒ
657
-
658
- programsListElement.innerHTML = `
659
- <div class="error-message">
660
- <i class="fas fa-exclamation-circle"></i> ${errorMessage}
661
- <button class="retry-button" id="retryLoadProgramsBtn">
662
- <i class="fas fa-sync"></i> ๋‹ค์‹œ ์‹œ๋„
663
- </button>
664
- </div>
665
- `;
666
-
667
- // ์žฌ์‹œ๋„ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
668
- // ์ด์ „์— ์ถ”๊ฐ€๋œ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐํ•˜๊ณ  ๋‹ค์‹œ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•  ์ˆ˜ ์žˆ์Œ
669
  const retryBtn = document.getElementById('retryLoadProgramsBtn');
670
  if (retryBtn) {
671
- retryBtn.replaceWith(retryBtn.cloneNode(true)); // ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ ํŠธ๋ฆญ
 
672
  document.getElementById('retryLoadProgramsBtn').addEventListener('click', () => {
673
  console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์žฌ์‹œ๋„ ๋ฒ„ํŠผ ํด๋ฆญ');
674
  this.loadProgramsList();
675
  });
676
  }
677
-
678
  },
679
 
680
  // ์‹คํ–‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ (๋ฏธ๋ฆฌ ์ •์˜๋œ ํ”„๋กœ๊ทธ๋žจ ์šฉ)
681
  showExecuteResult: function(status, message) {
682
  const resultElement = this.elements.executeResult;
683
- if (!resultElement) return; // ์š”์†Œ ์—†์œผ๋ฉด ์ข…๋ฃŒ
684
 
685
- // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
686
- resultElement.classList.remove('success', 'error', 'warning');
687
 
688
- // ๋‚ด์šฉ ์ดˆ๊ธฐํ™”
689
- resultElement.innerHTML = '';
690
-
691
- // ์ƒํƒœ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ
692
  switch (status) {
693
  case 'success':
694
  resultElement.classList.add('success');
@@ -710,18 +578,14 @@ const DeviceControl = {
710
  }
711
  },
712
 
713
- // ================== ์ถ”๊ฐ€ ์‹œ์ž‘ 4/4 ==================
714
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
715
  executeCustomProgram: async function(command) {
716
  if (!this.isConnected) {
717
  this.showCustomExecuteResult('error', '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
718
- console.error('์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
719
  return;
720
  }
721
-
722
  if (!command || command.trim() === '') {
723
  this.showCustomExecuteResult('error', '์˜ค๋ฅ˜: ์‹คํ–‰ํ•  ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
724
- console.error('์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ๋ช…๋ น์–ด ์—†์Œ');
725
  return;
726
  }
727
 
@@ -731,58 +595,38 @@ const DeviceControl = {
731
 
732
  try {
733
  console.log(`์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์ „์†ก: ${command}`);
734
-
735
  // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
736
  const response = await AppUtils.fetchWithTimeout('/api/device/execute-custom', {
737
  method: 'POST',
738
- headers: {
739
- 'Content-Type': 'application/json'
740
- },
741
  body: JSON.stringify({ command: command })
742
  }, 15000); // 15์ดˆ ํƒ€์ž„์•„์›ƒ
743
 
744
  const data = await response.json();
745
-
746
  if (response.ok && data.success) {
747
- // ์‹คํ–‰ ์„ฑ๊ณต
748
  console.log('์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต:', data);
749
-
750
- // ๊ฒฐ๊ณผ ํ‘œ์‹œ
751
  let successMessage = `๋ช…๋ น์–ด ์‹คํ–‰ ์„ฑ๊ณต: ${data.message || ''}`;
752
-
753
- // ์ถœ๋ ฅ ๋‚ด์šฉ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€ (HTML ์•ˆ์ „ ์ฒ˜๋ฆฌ ํฌํ•จ)
754
  if (data.output && data.output.trim()) {
755
  successMessage += `<div class="command-output"><pre>${AppUtils.escapeHtml(data.output)}</pre></div>`;
756
  }
757
-
758
  this.showCustomExecuteResult('success', successMessage);
759
-
760
- // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
761
  AppUtils.addSystemNotification(`์‚ฌ์šฉ์ž ์ •์˜ ๋ช…๋ น์–ด ์‹คํ–‰ ์„ฑ๊ณต: ${command}`);
762
  } else {
763
- // ์‹คํ–‰ ์‹คํŒจ
764
  console.error('์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹คํŒจ:', data);
765
-
766
  let errorMessage = `์‹คํ–‰ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`;
767
-
768
- // ์˜ค๋ฅ˜ ์ถœ๋ ฅ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€ (HTML ์•ˆ์ „ ์ฒ˜๋ฆฌ ํฌํ•จ)
769
  if (data.error_output && data.error_output.trim()) {
770
  errorMessage += `<div class="command-error"><pre>${AppUtils.escapeHtml(data.error_output)}</pre></div>`;
771
  }
772
-
773
  this.showCustomExecuteResult('error', errorMessage);
774
  }
775
  } catch (error) {
776
- // ์˜ˆ์™ธ ๋ฐœ์ƒ
777
  console.error('์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
778
-
779
  if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
780
  this.showCustomExecuteResult('error', '๋ช…๋ น์–ด ์‹คํ–‰ ์š”์ฒญ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„ ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.');
781
  } else {
782
  this.showCustomExecuteResult('error', `๋ช…๋ น์–ด ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
783
  }
784
  } finally {
785
- // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
786
  if(this.elements.executeCustomBtn) this.elements.executeCustomBtn.disabled = false;
787
  }
788
  },
@@ -790,19 +634,14 @@ const DeviceControl = {
790
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ
791
  showCustomExecuteResult: function(status, message) {
792
  const resultElement = this.elements.customExecuteResult;
793
- if (!resultElement) return; // ์š”์†Œ ์—†์œผ๋ฉด ์ข…๋ฃŒ
794
-
795
- // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
796
- resultElement.classList.remove('success', 'error', 'warning');
797
 
798
- // ๋‚ด์šฉ ์ดˆ๊ธฐํ™”
799
- resultElement.innerHTML = '';
800
 
801
- // ์ƒํƒœ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ
802
  switch (status) {
803
  case 'success':
804
  resultElement.classList.add('success');
805
- // HTML ๋ฉ”์‹œ์ง€ ์‚ฝ์ž… ์‹œ ์ฃผ์˜ (innerHTML ์‚ฌ์šฉ)
806
  resultElement.innerHTML = `<i class="fas fa-check-circle"></i> ${message}`;
807
  break;
808
  case 'error':
@@ -814,15 +653,177 @@ const DeviceControl = {
814
  resultElement.innerHTML = `<i class="fas fa-exclamation-triangle"></i> ${message}`;
815
  break;
816
  case 'loading':
817
- // ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ํ•จ์ˆ˜๊ฐ€ AppUtils์— ์žˆ๋‹ค๊ณ  ๊ฐ€์ •
818
  resultElement.innerHTML = `${AppUtils.createLoadingSpinner()} ${message}`;
819
  break;
820
  default:
821
  resultElement.textContent = message; // ๊ธฐ๋ณธ์€ ํ…์ŠคํŠธ๋งŒ ํ‘œ์‹œ
822
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
823
  }
824
- // ================== ์ถ”๊ฐ€ ๋ 4/4 ====================
825
- };
 
826
 
827
  // ํŽ˜์ด์ง€ ๋กœ๋“œ ์™„๋ฃŒ ์‹œ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
828
  document.addEventListener('DOMContentLoaded', function() {
 
1
  /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์žฅ์น˜ ์ œ์–ด JavaScript (ํฌํŠธ ์Šค์บ” ๊ธฐ๋Šฅ ์ถ”๊ฐ€๋จ)
3
  */
4
 
5
  // ์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ
 
27
  deviceStatusResult: null,
28
 
29
  // ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ (๋ฏธ๋ฆฌ ์ •์˜๋œ)
30
+ deviceProgramControl: null,
31
  getProgramsBtn: null,
32
  programsList: null,
33
  programSelectDropdown: null,
34
  executeProgramBtn: null,
35
  executeResult: null,
36
 
 
37
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
38
  deviceCustomControl: null,
39
  customCommandInput: null,
40
  executeCustomBtn: null,
41
+ customExecuteResult: null,
42
+
43
+ // ================== ํฌํŠธ ์Šค์บ” ์š”์†Œ ์ถ”๊ฐ€ ==================
44
+ devicePortScanControl: null, // ํฌํŠธ ์Šค์บ” ์„น์…˜ ์ปจํ…Œ์ด๋„ˆ
45
+ scanPortsBtn: null, // ํฌํŠธ ์Šค์บ” ๋ฒ„ํŠผ
46
+ portsStatus: null, // ํฌํŠธ ์Šค์บ” ์ƒํƒœ ํ‘œ์‹œ ์˜์—ญ
47
+ portsResults: null // ํฌํŠธ ์Šค์บ” ๊ฒฐ๊ณผ ํ‘œ์‹œ ์˜์—ญ
48
+ // ================== ์ถ”๊ฐ€ ๋ ============================
49
  },
50
 
51
  // ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
52
  init: function() {
53
  console.log('์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™” ์ค‘...');
 
 
54
  this.initElements();
 
 
55
  this.initEventListeners();
 
56
  console.log('์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
57
  },
58
 
 
80
  this.elements.executeProgramBtn = document.getElementById('executeProgramBtn');
81
  this.elements.executeResult = document.getElementById('executeResult');
82
 
 
83
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
84
  this.elements.deviceCustomControl = document.getElementById('deviceCustomControl');
85
  this.elements.customCommandInput = document.getElementById('customCommandInput');
86
  this.elements.executeCustomBtn = document.getElementById('executeCustomBtn');
87
  this.elements.customExecuteResult = document.getElementById('customExecuteResult');
88
+
89
+ // ================== ํฌํŠธ ์Šค์บ” ์š”์†Œ ์ดˆ๊ธฐํ™” ์ถ”๊ฐ€ ==================
90
+ this.elements.devicePortScanControl = document.getElementById('devicePortScanControl');
91
+ this.elements.scanPortsBtn = document.getElementById('scanPortsBtn');
92
+ this.elements.portsStatus = document.getElementById('portsStatus');
93
+ this.elements.portsResults = document.getElementById('portsResults');
94
+ // ================== ์ถ”๊ฐ€ ๋ =================================
95
 
96
  console.log('์žฅ์น˜ ์ œ์–ด DOM ์š”์†Œ ์ฐธ์กฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
97
  },
 
158
  });
159
  }
160
 
 
161
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ๋ฒ„ํŠผ ํด๋ฆญ
162
  if (this.elements.executeCustomBtn) {
163
  this.elements.executeCustomBtn.addEventListener('click', () => {
 
178
  }
179
  });
180
  }
181
+
182
+ // ================== ํฌํŠธ ์Šค์บ” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€ ==================
183
+ if (this.elements.scanPortsBtn) {
184
+ this.elements.scanPortsBtn.addEventListener('click', () => {
185
+ console.log('์žฅ์น˜ ํฌํŠธ ์Šค์บ” ๋ฒ„ํŠผ ํด๋ฆญ');
186
+ this.scanDevicePorts();
187
+ });
188
+ }
189
+ // ================== ์ถ”๊ฐ€ ๋ ==================================
190
 
191
  console.log('์žฅ์น˜ ์ œ์–ด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก ์™„๋ฃŒ');
192
  },
 
201
  tabContents.forEach(content => content.classList.remove('active'));
202
 
203
  // ์žฅ์น˜ ์ œ์–ด ํƒญ ํ™œ์„ฑํ™”
204
+ if(this.elements.deviceTab) this.elements.deviceTab.classList.add('active');
205
+ if(this.elements.deviceSection) this.elements.deviceSection.classList.add('active');
206
 
207
  console.log('์žฅ์น˜ ์ œ์–ด ํƒญ์œผ๋กœ ์ „ํ™˜ ์™„๋ฃŒ');
208
  },
 
213
  const inputUrl = this.elements.deviceServerUrlInput.value.trim();
214
 
215
  // ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘ UI ์—…๋ฐ์ดํŠธ
216
+ if(this.elements.connectDeviceServerBtn) this.elements.connectDeviceServerBtn.disabled = true;
217
  this.updateConnectionStatus('connecting', 'ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์ €์žฅ๋œ ์„œ๋ฒ„๋กœ ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘...');
218
 
219
  try {
 
227
  const data = await response.json();
228
 
229
  if (response.ok && data.success) {
230
+ // ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์„ฑ๊ณต
231
  console.log('ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต:', data);
232
  this.isConnected = true;
233
+ // ์„œ๋ฒ„ ์‘๋‹ต์—์„œ ์ƒํƒœ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
234
+ const serverStatusMsg = data?.data?.status || data?.server_status || '์ •์ƒ';
235
+ this.updateConnectionStatus('connected', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! ์ƒํƒœ: ${serverStatusMsg}`);
236
 
237
  // ๊ธฐ๋Šฅ UI ํ™œ์„ฑํ™”
238
  if(this.elements.deviceBasicFunctions) this.elements.deviceBasicFunctions.classList.add('active');
239
  if(this.elements.deviceProgramControl) this.elements.deviceProgramControl.classList.add('active');
 
240
  if(this.elements.deviceCustomControl) this.elements.deviceCustomControl.classList.add('active');
241
+ // ================== ํฌํŠธ ์Šค์บ” UI ํ™œ์„ฑํ™” ์ถ”๊ฐ€ ==================
242
+ if(this.elements.devicePortScanControl) this.elements.devicePortScanControl.classList.add('active');
243
+ // ================== ์ถ”๊ฐ€ ๋ ==================================
244
 
245
  // ์žฅ์น˜ ์ƒํƒœ ์ž๋™ ์ฒดํฌ
246
  this.checkDeviceStatus();
 
247
  // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ž๋™ ๋กœ๋“œ
248
  this.loadProgramsList();
 
249
  // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
250
  AppUtils.addSystemNotification(`์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! (ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL)`);
251
+
252
  } else {
253
  // ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹คํŒจ, ์ž…๋ ฅ๋œ URL๋กœ ์‹œ๋„
254
  console.warn('ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ, ์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค:', data);
 
 
255
  if (!inputUrl) {
256
  console.error('์ž…๋ ฅ๋œ URL์ด ์—†์–ด ์—ฐ๊ฒฐ ์‹คํŒจ');
257
  this.isConnected = false;
258
  this.updateConnectionStatus('error', 'ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹คํŒจ ๋ฐ ์ž…๋ ฅ๋œ URL์ด ์—†์Šต๋‹ˆ๋‹ค. URL์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
259
+ return; // ์ž…๋ ฅ URL ์—†์œผ๋ฉด ์—ฌ๊ธฐ์„œ ์ข…๋ฃŒ
260
  }
 
261
  // ์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„
262
+ await this.connectWithCustomUrl(inputUrl);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  }
264
  } catch (error) {
265
+ // ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ
266
+ console.error('ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
267
  this.isConnected = false;
 
 
268
  if (inputUrl) {
269
  console.warn('ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ, ์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค');
270
+ await this.connectWithCustomUrl(inputUrl); // ์ž…๋ ฅ URL ์žฌ์‹œ๋„
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  } else {
272
+ // ์ž…๋ ฅ URL๋„ ์—†๋Š” ๊ฒฝ์šฐ ์ตœ์ข… ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
273
  if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
274
  this.updateConnectionStatus('error', 'ํ™˜๊ฒฝ๋ณ€์ˆ˜ URL ์—ฐ๊ฒฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ. URL์„ ์ž…๋ ฅํ•˜์—ฌ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
275
  } else {
 
278
  }
279
  } finally {
280
  // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
281
+ if(this.elements.connectDeviceServerBtn) this.elements.connectDeviceServerBtn.disabled = false;
282
  }
283
  },
284
 
285
+ // ์‚ฌ์šฉ์ž ์ž…๋ ฅ URL๋กœ ์—ฐ๊ฒฐ ์‹œ๋„ (connectServer ๋‚ด๋ถ€ ๋กœ์ง ๋ถ„๋ฆฌ)
286
+ connectWithCustomUrl: async function(url) {
287
+ this.updateConnectionStatus('connecting', `์ž…๋ ฅ URL(${url})๋กœ ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘...`);
288
+ console.log(`์ž…๋ ฅํ•œ URL๋กœ ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๋„: ${url}`);
289
+
290
+ try {
291
+ // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ - Flask ๋ฐฑ์—”๋“œ๊ฐ€ URL ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌ
292
+ const customUrlResponse = await AppUtils.fetchWithTimeout('/api/device/connect', {
293
+ method: 'POST',
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({ url: url })
296
+ }, 10000);
297
+
298
+ const customUrlData = await customUrlResponse.json();
299
+
300
+ if (customUrlResponse.ok && customUrlData.success) {
301
+ // ์ž…๋ ฅ URL ์—ฐ๊ฒฐ ์„ฑ๊ณต
302
+ console.log('์ž…๋ ฅ URL ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต:', customUrlData);
303
+ this.isConnected = true;
304
+ this.updateConnectionStatus('connected', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! ์ƒํƒœ: ${customUrlData.server_status || '์ •์ƒ'}`);
305
+
306
+ // ๊ธฐ๋Šฅ UI ํ™œ์„ฑํ™”
307
+ if(this.elements.deviceBasicFunctions) this.elements.deviceBasicFunctions.classList.add('active');
308
+ if(this.elements.deviceProgramControl) this.elements.deviceProgramControl.classList.add('active');
309
+ if(this.elements.deviceCustomControl) this.elements.deviceCustomControl.classList.add('active');
310
+ // ================== ํฌํŠธ ์Šค์บ” UI ํ™œ์„ฑํ™” ์ถ”๊ฐ€ ==================
311
+ if(this.elements.devicePortScanControl) this.elements.devicePortScanControl.classList.add('active');
312
+ // ================== ์ถ”๊ฐ€ ๋ ==================================
313
+
314
+ // ์žฅ์น˜ ์ƒํƒœ ์ž๋™ ์ฒดํฌ
315
+ this.checkDeviceStatus();
316
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ž๋™ ๋กœ๋“œ
317
+ this.loadProgramsList();
318
+ // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
319
+ AppUtils.addSystemNotification(`์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! (${url})`);
320
+ } else {
321
+ // ์ž…๋ ฅ URL ์—ฐ๊ฒฐ ์‹คํŒจ
322
+ console.error('์ž…๋ ฅ URL ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ:', customUrlData);
323
+ this.isConnected = false;
324
+ this.updateConnectionStatus('error', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ: ${customUrlData.error || '์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜'}`);
325
+ }
326
+ } catch(error) {
327
+ // ์ž…๋ ฅ URL๋กœ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜
328
+ console.error('์ž…๋ ฅ URL๋กœ ์žฌ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
329
+ this.isConnected = false;
330
+ if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
331
+ this.updateConnectionStatus('error', '์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.');
332
+ } else {
333
+ this.updateConnectionStatus('error', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜: ${error.message}`);
334
+ }
335
+ }
336
+ },
337
+
338
  // ์—ฐ๊ฒฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
339
  updateConnectionStatus: function(status, message) {
340
  const statusElement = this.elements.deviceConnectionStatus;
 
342
 
343
  // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
344
  statusElement.classList.remove('connected', 'disconnected', 'error', 'connecting');
 
345
  // ์ƒํƒœ์— ๋”ฐ๋ผ ํด๋ž˜์Šค ์ถ”๊ฐ€
346
  statusElement.classList.add(status);
 
347
  // ๋ฉ”์‹œ์ง€ ์—…๋ฐ์ดํŠธ
348
  statusElement.textContent = message;
 
349
  console.log(`์—ฐ๊ฒฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ: ${status} - ${message}`);
350
  },
351
 
352
  // ์žฅ์น˜ ์ƒํƒœ ํ™•์ธ
353
  checkDeviceStatus: async function() {
354
  if (!this.isConnected) {
355
+ if(this.elements.deviceStatusResult) this.elements.deviceStatusResult.value = '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.';
356
  console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
357
  return;
358
  }
 
359
  // ์ƒํƒœ ํ™•์ธ ์ค‘ UI ์—…๋ฐ์ดํŠธ
360
+ if(this.elements.checkDeviceStatusBtn) this.elements.checkDeviceStatusBtn.disabled = true;
361
+ if(this.elements.deviceStatusResult) this.elements.deviceStatusResult.value = '์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘...';
362
 
363
  try {
364
  console.log('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ ์ „์†ก');
 
365
  // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
366
+ const response = await AppUtils.fetchWithTimeout('/api/device/status', { method: 'GET' });
 
 
 
367
  const data = await response.json();
368
 
369
  if (response.ok && data.success) {
 
370
  console.log('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์„ฑ๊ณต:', data);
371
  this.isStatusChecked = true;
372
  // JSON ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๊ธฐ ์ข‹๊ฒŒ ํฌ๋งทํŒ…ํ•˜์—ฌ ํ‘œ์‹œ
373
+ if(this.elements.deviceStatusResult) this.elements.deviceStatusResult.value = JSON.stringify(data.data || data, null, 2); // data.data ์šฐ์„  ํ™•์ธ
374
  } else {
 
375
  console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์‹คํŒจ:', data);
376
+ if(this.elements.deviceStatusResult) this.elements.deviceStatusResult.value = `์ƒํƒœ ํ™•์ธ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`;
377
  }
378
  } catch (error) {
 
379
  console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
380
+ if(this.elements.deviceStatusResult) this.elements.deviceStatusResult.value = `์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`;
381
  } finally {
382
+ if(this.elements.checkDeviceStatusBtn) this.elements.checkDeviceStatusBtn.disabled = false;
 
383
  }
384
  },
385
 
 
390
  console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
391
  return;
392
  }
 
 
393
  if (this.isLoadingPrograms) {
394
  console.log('์ด๋ฏธ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋”ฉ ์ค‘');
395
  return;
 
397
 
398
  // ๋กœ๋”ฉ ์ค‘ UI ์—…๋ฐ์ดํŠธ
399
  this.isLoadingPrograms = true;
400
+ if(this.elements.getProgramsBtn) this.elements.getProgramsBtn.disabled = true;
401
+ if(this.elements.programsList) {
402
+ this.elements.programsList.innerHTML = `<div class="loading-message">${AppUtils.createLoadingSpinner()} ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์ค‘...</div>`;
 
 
 
 
403
  }
404
 
 
405
  try {
406
  console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์š”์ฒญ ์ „์†ก');
 
407
  // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
408
+ const response = await AppUtils.fetchWithTimeout('/api/device/programs', { method: 'GET' });
 
 
 
409
  const data = await response.json();
410
 
411
  if (response.ok && data.success) {
 
412
  console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต:', data);
413
  this.programsList = data.programs || [];
 
 
414
  this.displayProgramsList();
 
 
415
  this.updateProgramsDropdown();
 
 
416
  this.updateExecuteButton();
417
  } else {
 
418
  console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:', data);
419
  this.showProgramsError(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`);
420
  }
421
  } catch (error) {
 
422
  console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
423
  this.showProgramsError(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
424
  } finally {
 
425
  this.isLoadingPrograms = false;
426
+ if(this.elements.getProgramsBtn) this.elements.getProgramsBtn.disabled = false;
427
  }
428
  },
429
 
430
  // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
431
  displayProgramsList: function() {
432
  const programsListElement = this.elements.programsList;
433
+ if (!programsListElement) return;
434
 
435
  if (!this.programsList || this.programsList.length === 0) {
436
+ programsListElement.innerHTML = `<div class="no-programs-message"><i class="fas fa-info-circle"></i> ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ์ด ์—†์Šต๋‹ˆ๋‹ค.</div>`;
 
 
 
 
437
  return;
438
  }
 
439
  // ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
440
+ let html = `<table class="program-list"><thead><tr><th>์ด๋ฆ„</th><th>์„ค๋ช…</th><th>๊ฒฝ๋กœ</th></tr></thead><tbody>`;
 
 
 
 
 
 
 
 
 
 
 
 
441
  this.programsList.forEach(program => {
442
+ html += `<tr>
443
+ <td>${AppUtils.escapeHtml(program.name || '์•Œ ์ˆ˜ ์—†์Œ')}</td>
444
+ <td>${AppUtils.escapeHtml(program.description || '-')}</td>
445
+ <td>${AppUtils.escapeHtml(program.path || '-')}</td>
446
+ </tr>`;
 
 
447
  });
448
+ html += `</tbody></table><div style="margin-top: 10px; font-size: 0.9em; color: #666;">์ด ${this.programsList.length}๊ฐœ ํ”„๋กœ๊ทธ๋žจ</div>`;
 
 
 
 
 
 
 
 
449
  programsListElement.innerHTML = html;
450
  },
451
 
452
  // ํ”„๋กœ๊ทธ๋žจ ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
453
  updateProgramsDropdown: function() {
454
  const dropdown = this.elements.programSelectDropdown;
455
+ if (!dropdown) return;
 
 
 
456
 
457
+ dropdown.innerHTML = ''; // ๊ธฐ์กด ์˜ต์…˜ ์ œ๊ฑฐ
458
  const defaultOption = document.createElement('option');
459
  defaultOption.value = '';
460
+ defaultOption.textContent = this.programsList.length > 0 ? '-- ์‹คํ–‰ํ•  ํ”„๋กœ๊ทธ๋žจ ์„ ํƒ --' : '-- ํ”„๋กœ๊ทธ๋žจ ์—†์Œ --';
 
 
461
  dropdown.appendChild(defaultOption);
462
 
 
463
  this.programsList.forEach(program => {
464
  const option = document.createElement('option');
465
  option.value = program.id || '';
466
  option.textContent = program.name || '์•Œ ์ˆ˜ ์—†์Œ';
 
 
467
  if (program.description) {
468
  option.textContent += ` (${program.description})`;
469
  }
 
470
  dropdown.appendChild(option);
471
  });
472
  },
 
475
  updateExecuteButton: function() {
476
  const dropdown = this.elements.programSelectDropdown;
477
  const executeBtn = this.elements.executeProgramBtn;
478
+ if (!dropdown || !executeBtn) return;
479
+ executeBtn.disabled = !dropdown.value; // ์„ ํƒ๋œ ํ”„๋กœ๊ทธ๋žจ์ด ์žˆ์„ ๋•Œ๋งŒ ํ™œ์„ฑํ™”
 
 
480
  },
481
 
482
  // ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ (๋ฏธ๋ฆฌ ์ •์˜๋œ)
483
  executeProgram: async function(programId) {
484
  if (!this.isConnected) {
485
  this.showExecuteResult('error', '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
 
486
  return;
487
  }
 
488
  if (!programId) {
489
  this.showExecuteResult('error', '์˜ค๋ฅ˜: ์‹คํ–‰ํ•  ํ”„๋กœ๊ทธ๋žจ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.');
 
490
  return;
491
  }
492
 
 
496
 
497
  try {
498
  console.log(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์ „์†ก: ${programId}`);
 
499
  // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
500
  const response = await AppUtils.fetchWithTimeout(`/api/device/programs/${programId}/execute`, {
501
  method: 'POST',
502
+ headers: { 'Content-Type': 'application/json' },
 
 
503
  body: JSON.stringify({})
504
+ }, 15000); // 15์ดˆ ํƒ€์ž„์•„์›ƒ
505
 
506
  const data = await response.json();
 
507
  if (response.ok && data.success) {
 
508
  console.log('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต:', data);
509
  this.showExecuteResult('success', `์‹คํ–‰ ์„ฑ๊ณต: ${data.message || 'ํ”„๋กœ๊ทธ๋žจ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'}`);
 
 
510
  AppUtils.addSystemNotification(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต: ${this.getSelectedProgramName()}`);
511
  } else {
 
512
  console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹คํŒจ:', data);
513
  this.showExecuteResult('error', `์‹คํ–‰ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`);
514
  }
515
  } catch (error) {
 
516
  console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
 
517
  if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
518
  this.showExecuteResult('error', 'ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„ ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.');
519
  } else {
520
  this.showExecuteResult('error', `ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
521
  }
522
  } finally {
523
+ if(this.elements.executeProgramBtn) this.elements.executeProgramBtn.disabled = false;
 
524
  }
525
  },
526
 
527
  // ์„ ํƒ๋œ ํ”„๋กœ๊ทธ๋žจ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ
528
  getSelectedProgramName: function() {
529
  const dropdown = this.elements.programSelectDropdown;
530
+ if (!dropdown || dropdown.selectedIndex < 0) return '์•Œ ์ˆ˜ ์—†๋Š” ํ”„๋กœ๊ทธ๋žจ'; // ์ธ๋ฑ์Šค ์ฒดํฌ ์ถ”๊ฐ€
531
  const selectedOption = dropdown.options[dropdown.selectedIndex];
532
  return selectedOption ? selectedOption.textContent : '์•Œ ์ˆ˜ ์—†๋Š” ํ”„๋กœ๊ทธ๋žจ';
533
  },
 
535
  // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์˜ค๋ฅ˜ ํ‘œ์‹œ
536
  showProgramsError: function(errorMessage) {
537
  const programsListElement = this.elements.programsList;
538
+ if (!programsListElement) return;
539
+
540
+ programsListElement.innerHTML = `<div class="error-message"><i class="fas fa-exclamation-circle"></i> ${errorMessage} <button class="retry-button" id="retryLoadProgramsBtn"><i class="fas fa-sync"></i> ๋‹ค์‹œ ์‹œ๋„</button></div>`;
 
 
 
 
 
 
 
 
 
 
541
  const retryBtn = document.getElementById('retryLoadProgramsBtn');
542
  if (retryBtn) {
543
+ // ๊ธฐ์กด ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ ํ›„ ์ถ”๊ฐ€ (์ค‘๋ณต ๋ฐฉ์ง€)
544
+ retryBtn.replaceWith(retryBtn.cloneNode(true));
545
  document.getElementById('retryLoadProgramsBtn').addEventListener('click', () => {
546
  console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์žฌ์‹œ๋„ ๋ฒ„ํŠผ ํด๋ฆญ');
547
  this.loadProgramsList();
548
  });
549
  }
 
550
  },
551
 
552
  // ์‹คํ–‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ (๋ฏธ๋ฆฌ ์ •์˜๋œ ํ”„๋กœ๊ทธ๋žจ ์šฉ)
553
  showExecuteResult: function(status, message) {
554
  const resultElement = this.elements.executeResult;
555
+ if (!resultElement) return;
556
 
557
+ resultElement.classList.remove('success', 'error', 'warning'); // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
558
+ resultElement.innerHTML = ''; // ๋‚ด์šฉ ์ดˆ๊ธฐํ™”
559
 
 
 
 
 
560
  switch (status) {
561
  case 'success':
562
  resultElement.classList.add('success');
 
578
  }
579
  },
580
 
 
581
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
582
  executeCustomProgram: async function(command) {
583
  if (!this.isConnected) {
584
  this.showCustomExecuteResult('error', '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
 
585
  return;
586
  }
 
587
  if (!command || command.trim() === '') {
588
  this.showCustomExecuteResult('error', '์˜ค๋ฅ˜: ์‹คํ–‰ํ•  ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
 
589
  return;
590
  }
591
 
 
595
 
596
  try {
597
  console.log(`์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์ „์†ก: ${command}`);
 
598
  // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
599
  const response = await AppUtils.fetchWithTimeout('/api/device/execute-custom', {
600
  method: 'POST',
601
+ headers: { 'Content-Type': 'application/json' },
 
 
602
  body: JSON.stringify({ command: command })
603
  }, 15000); // 15์ดˆ ํƒ€์ž„์•„์›ƒ
604
 
605
  const data = await response.json();
 
606
  if (response.ok && data.success) {
 
607
  console.log('์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต:', data);
 
 
608
  let successMessage = `๋ช…๋ น์–ด ์‹คํ–‰ ์„ฑ๊ณต: ${data.message || ''}`;
 
 
609
  if (data.output && data.output.trim()) {
610
  successMessage += `<div class="command-output"><pre>${AppUtils.escapeHtml(data.output)}</pre></div>`;
611
  }
 
612
  this.showCustomExecuteResult('success', successMessage);
 
 
613
  AppUtils.addSystemNotification(`์‚ฌ์šฉ์ž ์ •์˜ ๋ช…๋ น์–ด ์‹คํ–‰ ์„ฑ๊ณต: ${command}`);
614
  } else {
 
615
  console.error('์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹คํŒจ:', data);
 
616
  let errorMessage = `์‹คํ–‰ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`;
 
 
617
  if (data.error_output && data.error_output.trim()) {
618
  errorMessage += `<div class="command-error"><pre>${AppUtils.escapeHtml(data.error_output)}</pre></div>`;
619
  }
 
620
  this.showCustomExecuteResult('error', errorMessage);
621
  }
622
  } catch (error) {
 
623
  console.error('์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
 
624
  if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
625
  this.showCustomExecuteResult('error', '๋ช…๋ น์–ด ์‹คํ–‰ ์š”์ฒญ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„ ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.');
626
  } else {
627
  this.showCustomExecuteResult('error', `๋ช…๋ น์–ด ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
628
  }
629
  } finally {
 
630
  if(this.elements.executeCustomBtn) this.elements.executeCustomBtn.disabled = false;
631
  }
632
  },
 
634
  // ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ
635
  showCustomExecuteResult: function(status, message) {
636
  const resultElement = this.elements.customExecuteResult;
637
+ if (!resultElement) return;
 
 
 
638
 
639
+ resultElement.classList.remove('success', 'error', 'warning'); // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
640
+ resultElement.innerHTML = ''; // ๋‚ด์šฉ ์ดˆ๊ธฐํ™”
641
 
 
642
  switch (status) {
643
  case 'success':
644
  resultElement.classList.add('success');
 
645
  resultElement.innerHTML = `<i class="fas fa-check-circle"></i> ${message}`;
646
  break;
647
  case 'error':
 
653
  resultElement.innerHTML = `<i class="fas fa-exclamation-triangle"></i> ${message}`;
654
  break;
655
  case 'loading':
 
656
  resultElement.innerHTML = `${AppUtils.createLoadingSpinner()} ${message}`;
657
  break;
658
  default:
659
  resultElement.textContent = message; // ๊ธฐ๋ณธ์€ ํ…์ŠคํŠธ๋งŒ ํ‘œ์‹œ
660
  }
661
+ },
662
+
663
+ // ================== ํฌํŠธ ์Šค์บ” ํ•จ์ˆ˜ ์ถ”๊ฐ€ ==================
664
+ // ์žฅ์น˜ ํฌํŠธ ์Šค์บ” ํ•จ์ˆ˜
665
+ scanDevicePorts: async function() {
666
+ if (!this.isConnected) {
667
+ this.showPortsStatus('error', '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
668
+ console.error('์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
669
+ return;
670
+ }
671
+ // ์Šค์บ” ์ค‘ UI ์—…๋ฐ์ดํŠธ
672
+ if(this.elements.scanPortsBtn) this.elements.scanPortsBtn.disabled = true;
673
+ this.showPortsStatus('loading', '์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์ค‘...');
674
+
675
+ try {
676
+ console.log('์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์š”์ฒญ ์ „์†ก');
677
+ // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
678
+ const response = await AppUtils.fetchWithTimeout('/api/device/scan-ports', {
679
+ method: 'GET'
680
+ }, 30000); // ์žฅ์น˜ ์Šค์บ”์€ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํƒ€์ž„์•„์›ƒ 30์ดˆ๋กœ ์„ค์ •
681
+
682
+ const data = await response.json();
683
+
684
+ if (response.ok && data.success) {
685
+ // ์Šค์บ” ์„ฑ๊ณต
686
+ console.log('์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์„ฑ๊ณต:', data);
687
+ // ๊ฒฐ๊ณผ ํ‘œ์‹œ
688
+ this.displayPortsResults(data);
689
+ this.showPortsStatus('success', '์žฅ์น˜ ํฌํŠธ ์Šค์บ”์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
690
+ } else {
691
+ // ์Šค์บ” ์‹คํŒจ
692
+ console.error('์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์‹คํŒจ:', data);
693
+ this.showPortsStatus('error', `์Šค์บ” ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`);
694
+ }
695
+ } catch (error) {
696
+ // ์˜ˆ์™ธ ๋ฐœ์ƒ
697
+ console.error('์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
698
+ if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
699
+ this.showPortsStatus('error', '์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์š”์ฒญ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„ ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.');
700
+ } else {
701
+ this.showPortsStatus('error', `์žฅ์น˜ ํฌํŠธ ์Šค์บ” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
702
+ }
703
+ } finally {
704
+ // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
705
+ if(this.elements.scanPortsBtn) this.elements.scanPortsBtn.disabled = false;
706
+ }
707
+ },
708
+
709
+ // ์žฅ์น˜ ํฌํŠธ ์Šค์บ” ๊ฒฐ๊ณผ ํ‘œ์‹œ
710
+ displayPortsResults: function(data) {
711
+ const portsResultsElement = this.elements.portsResults;
712
+ if (!portsResultsElement) return; // ์š”์†Œ ์—†์œผ๋ฉด ์ข…๋ฃŒ
713
+
714
+ // ์‹œ์Šคํ…œ ์ •๋ณด
715
+ const systemInfo = data.system_info || {};
716
+ const devices = data.devices || {};
717
+ const serialPorts = devices.serial || [];
718
+ const usbDevices = devices.usb || [];
719
+ const windowsUsbDevices = devices.windows_usb || []; // windows_usb ํ‚ค ์ถ”๊ฐ€
720
+
721
+ // HTML ์ƒ์„ฑ ์‹œ์ž‘
722
+ let html = `<div class="ports-results-container">`;
723
+
724
+ // ์‹œ์Šคํ…œ ์ •๋ณด ์„น์…˜
725
+ html += `<div class="ports-section">
726
+ <h4>์‹œ์Šคํ…œ ์ •๋ณด</h4>
727
+ <div class="info-row"><span class="info-label">OS:</span><span class="info-value">${AppUtils.escapeHtml(systemInfo.os || '์•Œ ์ˆ˜ ์—†์Œ')} ${AppUtils.escapeHtml(systemInfo.version || '')}</span></div>
728
+ <div class="info-row"><span class="info-label">์‹œ์Šคํ…œ:</span><span class="info-value">${AppUtils.escapeHtml(systemInfo.platform || '์•Œ ์ˆ˜ ์—†์Œ')}</span></div>
729
+ <div class="info-row"><span class="info-label">์กฐํšŒ ์‹œ๊ฐ„:</span><span class="info-value">${AppUtils.escapeHtml(data.timestamp || '์•Œ ์ˆ˜ ์—†์Œ')}</span></div>
730
+ </div>`;
731
+
732
+ // ์‹œ๋ฆฌ์–ผ ํฌํŠธ ์„น์…˜
733
+ html += `<div class="ports-section"><h4>์‹œ๋ฆฌ์–ผ ํฌํŠธ (${serialPorts.length}๊ฐœ)</h4>`;
734
+ if (serialPorts.length === 0) {
735
+ html += `<p class="no-ports-message">๋ฐœ๊ฒฌ๋œ ์‹œ๋ฆฌ์–ผ ํฌํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>`;
736
+ } else {
737
+ html += `<table class="ports-table"><thead><tr><th>ํฌํŠธ</th><th>์„ค๋ช…</th><th>์ œ์กฐ์‚ฌ</th><th>VID:PID</th></tr></thead><tbody>`;
738
+ serialPorts.forEach(port => {
739
+ const vidPid = (port.vid && port.pid) ? `${port.vid}:${port.pid}` : '์•Œ ์ˆ˜ ์—†์Œ';
740
+ html += `<tr>
741
+ <td>${AppUtils.escapeHtml(port.port || '')}</td>
742
+ <td>${AppUtils.escapeHtml(port.description || '')}</td>
743
+ <td>${AppUtils.escapeHtml(port.manufacturer || '')}</td>
744
+ <td>${AppUtils.escapeHtml(vidPid)}</td>
745
+ </tr>`;
746
+ });
747
+ html += `</tbody></table>`;
748
+ }
749
+ html += `</div>`;
750
+
751
+ // USB ์žฅ์น˜ ์„น์…˜ (pyserial list_ports ๊ธฐ๋ฐ˜)
752
+ html += `<div class="ports-section"><h4>USB ์žฅ์น˜ (pyserial) (${usbDevices.length}๊ฐœ)</h4>`;
753
+ if (usbDevices.length === 0) {
754
+ html += `<p class="no-ports-message">๋ฐœ๊ฒฌ๋œ USB ์žฅ์น˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>`;
755
+ } else {
756
+ html += `<table class="ports-table"><thead><tr><th>VID:PID</th><th>์ œํ’ˆ๋ช…</th><th>์ œ์กฐ์‚ฌ</th><th>์œ„์น˜</th></tr></thead><tbody>`;
757
+ usbDevices.forEach(device => {
758
+ const vidPid = (device.vid && device.pid) ? `${device.vid}:${device.pid}` : '์•Œ ์ˆ˜ ์—†์Œ';
759
+ html += `<tr>
760
+ <td>${AppUtils.escapeHtml(vidPid)}</td>
761
+ <td>${AppUtils.escapeHtml(device.product || '')}</td>
762
+ <td>${AppUtils.escapeHtml(device.manufacturer || '')}</td>
763
+ <td>${AppUtils.escapeHtml(device.location || '')}</td>
764
+ </tr>`;
765
+ });
766
+ html += `</tbody></table>`;
767
+ }
768
+ html += `</div>`;
769
+
770
+ // Windows WMI USB ์žฅ์น˜ ์„น์…˜ (์žˆ๋Š” ๊ฒฝ์šฐ)
771
+ if (windowsUsbDevices && windowsUsbDevices.length > 0) {
772
+ html += `<div class="ports-section"><h4>Windows USB ์žฅ์น˜ (WMI) (${windowsUsbDevices.length}๊ฐœ)</h4>`;
773
+ html += `<table class="ports-table"><thead><tr><th>์ด๋ฆ„</th><th>์„ค๋ช…</th><th>์ œ์กฐ์‚ฌ</th><th>VID:PID</th></tr></thead><tbody>`;
774
+ windowsUsbDevices.forEach(device => {
775
+ const vidPid = (device.vid && device.pid) ? `${device.vid}:${device.pid}` : '์•Œ ์ˆ˜ ์—†์Œ';
776
+ html += `<tr>
777
+ <td>${AppUtils.escapeHtml(device.name || '')}</td>
778
+ <td>${AppUtils.escapeHtml(device.description || '')}</td>
779
+ <td>${AppUtils.escapeHtml(device.manufacturer || '')}</td>
780
+ <td>${AppUtils.escapeHtml(vidPid)}</td>
781
+ </tr>`;
782
+ });
783
+ html += `</tbody></table></div>`;
784
+ }
785
+
786
+ // ์ปจํ…Œ์ด๋„ˆ ๋‹ซ๊ธฐ
787
+ html += `</div>`;
788
+
789
+ // HTML ์„ค์ •
790
+ portsResultsElement.innerHTML = html;
791
+ },
792
+
793
+ // ํฌํŠธ ์Šค์บ” ์ƒํƒœ ํ‘œ์‹œ
794
+ showPortsStatus: function(status, message) {
795
+ const statusElement = this.elements.portsStatus;
796
+ if (!statusElement) return; // ์š”์†Œ ์—†์œผ๋ฉด ์ข…๋ฃŒ
797
+
798
+ // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
799
+ statusElement.classList.remove('success', 'error', 'warning');
800
+ // ๋‚ด์šฉ ์ดˆ๊ธฐํ™”
801
+ statusElement.innerHTML = '';
802
+
803
+ // ์ƒํƒœ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ
804
+ switch (status) {
805
+ case 'success':
806
+ statusElement.classList.add('success');
807
+ statusElement.innerHTML = `<i class="fas fa-check-circle"></i> ${message}`;
808
+ break;
809
+ case 'error':
810
+ statusElement.classList.add('error');
811
+ statusElement.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${message}`;
812
+ break;
813
+ case 'warning':
814
+ statusElement.classList.add('warning');
815
+ statusElement.innerHTML = `<i class="fas fa-exclamation-triangle"></i> ${message}`;
816
+ break;
817
+ case 'loading':
818
+ statusElement.innerHTML = `${AppUtils.createLoadingSpinner()} ${message}`;
819
+ break;
820
+ default:
821
+ statusElement.textContent = message;
822
+ }
823
  }
824
+ // ================== ์ถ”๊ฐ€ ๋ ============================
825
+
826
+ }; // DeviceControl ๊ฐ์ฒด ๋
827
 
828
  // ํŽ˜์ด์ง€ ๋กœ๋“œ ์™„๋ฃŒ ์‹œ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
829
  document.addEventListener('DOMContentLoaded', function() {
app/templates/index.html CHANGED
@@ -16,8 +16,7 @@
16
  <div class="llm-selector">
17
  <label for="llmSelect">LLM ์„ ํƒ:</label>
18
  <select id="llmSelect">
19
- <!-- ์˜ต์…˜์€ JavaScript์—์„œ ๋™์ ์œผ๋กœ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค -->
20
- </select>
21
  </div>
22
  <div class="user-info">
23
  <span>์‚ฌ์šฉ์ž: {% if session.username %}{{ session.username }}{% else %}์†๋‹˜{% endif %}</span>
@@ -34,7 +33,6 @@
34
  </header>
35
 
36
  <main>
37
- <!-- ๋Œ€ํ™” ํƒญ -->
38
  <section id="chatSection" class="tab-content active">
39
  <div class="chat-container">
40
  <div class="chat-messages" id="chatMessages">
@@ -44,7 +42,7 @@
44
  </div>
45
  </div>
46
  </div>
47
-
48
  <div class="chat-input-container">
49
  <textarea id="userInput" placeholder="๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”..." rows="1"></textarea>
50
  <button id="micButton" class="mic-button">
@@ -54,7 +52,7 @@
54
  <i class="fas fa-paper-plane"></i>
55
  </button>
56
  </div>
57
-
58
  <div id="recordingStatus" class="recording-status hidden">
59
  <div class="recording-indicator">
60
  <div class="recording-pulse"></div>
@@ -67,34 +65,33 @@
67
  </div>
68
  </section>
69
 
70
- <!-- ๋ฌธ์„œ๊ด€๋ฆฌ ํƒญ -->
71
  <section id="docsSection" class="tab-content">
72
  <div class="docs-container">
73
  <div class="upload-section">
74
  <h2>๋ฌธ์„œ ์—…๋กœ๋“œ</h2>
75
  <p>์ง€์‹๋ฒ ์ด์Šค์— ์ถ”๊ฐ€ํ•  ๋ฌธ์„œ๋ฅผ ์—…๋กœ๋“œํ•˜์„ธ์š”. (์ง€์› ํ˜•์‹: .txt, .md, .csv)</p>
76
-
77
  <form id="uploadForm" enctype="multipart/form-data">
78
  <div class="file-upload">
79
  <input type="file" id="documentFile" name="document" accept=".txt,.md,.csv">
80
  <label for="documentFile">ํŒŒ์ผ ์„ ํƒ</label>
81
  <span id="fileName">์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ</span>
82
  </div>
83
-
84
  <button type="submit" id="uploadButton" class="upload-button">
85
  <i class="fas fa-upload"></i> ์—…๋กœ๋“œ
86
  </button>
87
  </form>
88
-
89
  <div id="uploadStatus" class="upload-status hidden"></div>
90
  </div>
91
-
92
  <div class="docs-list-section">
93
  <h2>๋ฌธ์„œ ๋ชฉ๋ก</h2>
94
  <button id="refreshDocsButton" class="refresh-button">
95
  <i class="fas fa-sync-alt"></i> ์ƒˆ๋กœ๊ณ ์นจ
96
  </button>
97
-
98
  <div class="docs-list-container">
99
  <table id="docsList" class="docs-list">
100
  <thead>
@@ -105,15 +102,14 @@
105
  </tr>
106
  </thead>
107
  <tbody>
108
- <!-- ๋ฌธ์„œ ๋ชฉ๋ก์ด ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
109
- </tbody>
110
  </table>
111
-
112
  <div id="docsLoading" class="loading-indicator">
113
  <div class="spinner"></div>
114
  <p>๋ฌธ์„œ ๋กœ๋”ฉ ์ค‘...</p>
115
  </div>
116
-
117
  <div id="noDocsMessage" class="no-docs-message hidden">
118
  <p>์ง€์‹๋ฒ ์ด์Šค์— ๋“ฑ๋ก๋œ ๋ฌธ์„œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ๋ฅผ ์—…๋กœ๋“œํ•ด ์ฃผ์„ธ์š”.</p>
119
  </div>
@@ -121,8 +117,7 @@
121
  </div>
122
  </div>
123
  </section>
124
-
125
- <!-- ์žฅ์น˜ ์ œ์–ด ํƒญ -->
126
  <section id="deviceSection" class="tab-content">
127
  <div class="device-connection">
128
  <h3>1. ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ</h3>
@@ -157,8 +152,7 @@
157
  <button id="executeProgramBtn" class="execute-btn" disabled>์„ ํƒํ•œ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰</button>
158
  <div id="executeResult" class="execute-result"></div>
159
  </div>
160
-
161
- <!-- ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„น์…˜ ์ถ”๊ฐ€ -->
162
  <div id="deviceCustomControl" class="program-control">
163
  <h3>4. ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰</h3>
164
  <div class="custom-command-container">
@@ -167,7 +161,13 @@
167
  </div>
168
  <div id="customExecuteResult" class="execute-result"></div>
169
  </div>
170
- </section>
 
 
 
 
 
 
171
  </main>
172
 
173
  <footer>
@@ -183,4 +183,4 @@
183
  <script src="{{ url_for('static', filename='js/app-device.js') }}"></script>
184
  <script src="{{ url_for('static', filename='js/app.js') }}"></script>
185
  </body>
186
- </html>
 
16
  <div class="llm-selector">
17
  <label for="llmSelect">LLM ์„ ํƒ:</label>
18
  <select id="llmSelect">
19
+ </select>
 
20
  </div>
21
  <div class="user-info">
22
  <span>์‚ฌ์šฉ์ž: {% if session.username %}{{ session.username }}{% else %}์†๋‹˜{% endif %}</span>
 
33
  </header>
34
 
35
  <main>
 
36
  <section id="chatSection" class="tab-content active">
37
  <div class="chat-container">
38
  <div class="chat-messages" id="chatMessages">
 
42
  </div>
43
  </div>
44
  </div>
45
+
46
  <div class="chat-input-container">
47
  <textarea id="userInput" placeholder="๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”..." rows="1"></textarea>
48
  <button id="micButton" class="mic-button">
 
52
  <i class="fas fa-paper-plane"></i>
53
  </button>
54
  </div>
55
+
56
  <div id="recordingStatus" class="recording-status hidden">
57
  <div class="recording-indicator">
58
  <div class="recording-pulse"></div>
 
65
  </div>
66
  </section>
67
 
 
68
  <section id="docsSection" class="tab-content">
69
  <div class="docs-container">
70
  <div class="upload-section">
71
  <h2>๋ฌธ์„œ ์—…๋กœ๋“œ</h2>
72
  <p>์ง€์‹๋ฒ ์ด์Šค์— ์ถ”๊ฐ€ํ•  ๋ฌธ์„œ๋ฅผ ์—…๋กœ๋“œํ•˜์„ธ์š”. (์ง€์› ํ˜•์‹: .txt, .md, .csv)</p>
73
+
74
  <form id="uploadForm" enctype="multipart/form-data">
75
  <div class="file-upload">
76
  <input type="file" id="documentFile" name="document" accept=".txt,.md,.csv">
77
  <label for="documentFile">ํŒŒ์ผ ์„ ํƒ</label>
78
  <span id="fileName">์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ</span>
79
  </div>
80
+
81
  <button type="submit" id="uploadButton" class="upload-button">
82
  <i class="fas fa-upload"></i> ์—…๋กœ๋“œ
83
  </button>
84
  </form>
85
+
86
  <div id="uploadStatus" class="upload-status hidden"></div>
87
  </div>
88
+
89
  <div class="docs-list-section">
90
  <h2>๋ฌธ์„œ ๋ชฉ๋ก</h2>
91
  <button id="refreshDocsButton" class="refresh-button">
92
  <i class="fas fa-sync-alt"></i> ์ƒˆ๋กœ๊ณ ์นจ
93
  </button>
94
+
95
  <div class="docs-list-container">
96
  <table id="docsList" class="docs-list">
97
  <thead>
 
102
  </tr>
103
  </thead>
104
  <tbody>
105
+ </tbody>
 
106
  </table>
107
+
108
  <div id="docsLoading" class="loading-indicator">
109
  <div class="spinner"></div>
110
  <p>๋ฌธ์„œ ๋กœ๋”ฉ ์ค‘...</p>
111
  </div>
112
+
113
  <div id="noDocsMessage" class="no-docs-message hidden">
114
  <p>์ง€์‹๋ฒ ์ด์Šค์— ๋“ฑ๋ก๋œ ๋ฌธ์„œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ๋ฅผ ์—…๋กœ๋“œํ•ด ์ฃผ์„ธ์š”.</p>
115
  </div>
 
117
  </div>
118
  </div>
119
  </section>
120
+
 
121
  <section id="deviceSection" class="tab-content">
122
  <div class="device-connection">
123
  <h3>1. ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ</h3>
 
152
  <button id="executeProgramBtn" class="execute-btn" disabled>์„ ํƒํ•œ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰</button>
153
  <div id="executeResult" class="execute-result"></div>
154
  </div>
155
+
 
156
  <div id="deviceCustomControl" class="program-control">
157
  <h3>4. ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰</h3>
158
  <div class="custom-command-container">
 
161
  </div>
162
  <div id="customExecuteResult" class="execute-result"></div>
163
  </div>
164
+
165
+ <div id="devicePortScanControl" class="program-control"> <h3>5. ์žฅ์น˜ ํฌํŠธ ์กฐํšŒ</h3>
166
+ <div class="function-buttons">
167
+ <button id="scanPortsBtn">์žฅ์น˜ ํฌํŠธ ์Šค์บ”</button>
168
+ </div>
169
+ <div id="portsStatus" class="ports-status"></div> <div id="portsResults" class="ports-results"></div> </div>
170
+ </section>
171
  </main>
172
 
173
  <footer>
 
183
  <script src="{{ url_for('static', filename='js/app-device.js') }}"></script>
184
  <script src="{{ url_for('static', filename='js/app.js') }}"></script>
185
  </body>
186
+ </html>
docs/project_plan.md CHANGED
@@ -24,10 +24,63 @@
24
  - `static/js/app-device.js`์— ์žฅ์น˜ ์ œ์–ด JavaScript ์ฝ”๋“œ
25
  - `static/css/device-style.css`์— ์žฅ์น˜ ์ œ์–ด ์Šคํƒ€์ผ
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  ## ๋‚จ์€ ์ž‘์—…
 
 
 
 
28
  - [ ] ์ „์ฒด ์‹œ์Šคํ…œ ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…
29
- - ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ (๊ฐœ๋ฐœ ํ™˜๊ฒฝ, ๋ฐฐํฌ ํ™˜๊ฒฝ)
30
- - ๋‹ค์–‘ํ•œ ์žฅ์น˜ ์—ฐ๊ฒฐ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ
31
 
32
  ## ํŒŒ์ผ ๊ตฌ์กฐ
33
  - `app_revised.py`: ๋ฉ”์ธ Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
@@ -54,6 +107,8 @@
54
  - `docs/`: ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ
55
  - `project_plan.md`: ํ”„๋กœ์ ํŠธ ๊ณ„ํš ๋ฌธ์„œ
56
  - `cleanup_plan.md`: ์ฝ”๋“œ ์ •๋ฆฌ ๊ณ„ํš ๋ฌธ์„œ
 
 
57
 
58
  ## LocalPCAgent ํ†ตํ•ฉ ์š”์•ฝ
59
  - **๋ฐฑ์—”๋“œ API**: `app/app_device_routes.py`์— ๊ตฌํ˜„
@@ -63,6 +118,7 @@
63
  - `/api/device/programs`: ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ
64
  - `/api/device/programs/<program_id>/execute`: ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
65
  - `/api/device/execute-custom`: ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
 
66
 
67
  - **ํ”„๋ŸฐํŠธ์—”๋“œ**: ๊ธฐ๋Šฅ ์™„์„ฑ
68
  - ์žฅ์น˜ ์ œ์–ด ํƒญ UI: `templates/index.html`์— ๊ตฌํ˜„
 
24
  - `static/js/app-device.js`์— ์žฅ์น˜ ์ œ์–ด JavaScript ์ฝ”๋“œ
25
  - `static/css/device-style.css`์— ์žฅ์น˜ ์ œ์–ด ์Šคํƒ€์ผ
26
 
27
+ ## ํ˜„์žฌ ์ƒํƒœ
28
+ ์ฝ”๋“œ ๊ฒ€ํ†  ๊ฒฐ๊ณผ ๋Œ€๋ถ€๋ถ„์˜ ํ†ตํ•ฉ ์ž‘์—…์ด ์ด๋ฏธ ์™„๋ฃŒ๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค:
29
+ - Flask ๋ฐฑ์—”๋“œ์— LocalPCAgent API ์—”๋“œํฌ์ธํŠธ๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (`app_device_routes.py`)
30
+ - ํ”„๋ก ํŠธ์—”๋“œ UI์— ์žฅ์น˜ ์ œ์–ด ํƒญ์ด ์ถ”๊ฐ€๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (`index.html`)
31
+ - ์žฅ์น˜ ์ œ์–ด๋ฅผ ์œ„ํ•œ JavaScript ๋กœ์ง์ด ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (`app-device.js`)
32
+ - ๊ธฐ๋ณธ ๊ธฐ๋Šฅ๊ณผ ์‚ฌ์šฉ์ž ์ •์˜ ๋ช…๋ น์–ด ์‹คํ–‰ ๊ธฐ๋Šฅ์ด ๋ชจ๋‘ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค
33
+
34
+ ## ์ถ”๊ฐ€ ๊ฐœ๋ฐœ ๊ณ„ํš: ์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
35
+
36
+ ### ๊ฐœ๋ฐœ ๋ชฉํ‘œ
37
+ ๋กœ์ปฌPC์— ์—ฐ๊ฒฐ๋œ ์žฅ์น˜(ํŠนํžˆ COM ํฌํŠธ์™€ USB ์žฅ์น˜)๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
38
+
39
+ ### ๊ตฌํ˜„ ์š”๊ตฌ์‚ฌํ•ญ
40
+ 1. **๋กœ์ปฌPC ์žฅ์น˜ ์กฐํšŒ Python ํ”„๋กœ๊ทธ๋žจ ๊ฐœ๋ฐœ**
41
+ - COM ํฌํŠธ ๋ฐ USB ์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ
42
+ - ์žฅ์น˜๋ณ„ ์ƒ์„ธ ์ •๋ณด(์ œ์กฐ์‚ฌ, ์ œํ’ˆ๋ช…, ์‹œ๋ฆฌ์–ผ ๋ฒˆํ˜ธ ๋“ฑ) ์ˆ˜์ง‘
43
+ - ๋ฉ€ํ‹ฐ ํ”Œ๋žซํผ ์ง€์›(Windows, Linux, macOS)
44
+
45
+ 2. **LocalPCAgent API ํ™•์žฅ**
46
+ - ์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
47
+ - ์กฐํšŒ ๊ฒฐ๊ณผ ํšจ์œจ์ ์ธ ์ „์†ก ๋ฐฉ์‹ ๊ตฌํ˜„
48
+
49
+ 3. **UI ํ™•์žฅ**
50
+ - ์žฅ์น˜ ์ œ์–ด ํƒญ์— ์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์„น์…˜ ์ถ”๊ฐ€
51
+ - ์กฐํšŒ ๊ฒฐ๊ณผ ํ‘œ์‹œ UI ๊ตฌํ˜„
52
+ - ์žฅ์น˜ ํƒ€์ž…๋ณ„ ํ•„ํ„ฐ๋ง ๊ธฐ๋Šฅ ๊ตฌํ˜„
53
+
54
+ ### ๊ธฐ์ˆ  ์Šคํƒ
55
+ - ๋ฐฑ์—”๋“œ: Python(`pyserial`, `pyusb`), Flask
56
+ - ํ”„๋ก ํŠธ์—”๋“œ: HTML, CSS, JavaScript
57
+ - ํ†ต์‹ : REST API, JSON ํฌ๋งท
58
+
59
+ ### ๊ตฌํ˜„ ๊ณ„ํš
60
+ 1. **๋กœ์ปฌPC ์žฅ์น˜ ์กฐํšŒ Python ์Šคํฌ๋ฆฝํŠธ ๊ฐœ๋ฐœ**
61
+ - `scan_devices.py` ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ
62
+ - ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…
63
+
64
+ 2. **LocalPCAgent์— ํ†ตํ•ฉ**
65
+ - LocalPCAgent ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก์— ๋“ฑ๋ก
66
+ - API ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ
67
+
68
+ 3. **Flask ๋ฐฑ์—”๋“œ API ํ™•์žฅ**
69
+ - `app_device_routes.py`์— ์žฅ์น˜ ์กฐํšŒ API ์ถ”๊ฐ€
70
+ - ๋ฐ์ดํ„ฐ ํฌ๋งท ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
71
+
72
+ 4. **ํ”„๋ก ํŠธ์—”๋“œ UI ๊ฐœ๋ฐœ**
73
+ - `index.html`์— ์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์„น์…˜ ์ถ”๊ฐ€
74
+ - `app-device.js`์— ์žฅ์น˜ ์กฐํšŒ ๊ด€๋ จ ํ•จ์ˆ˜ ์ถ”๊ฐ€
75
+ - ํ•„์š”์‹œ CSS ์Šคํƒ€์ผ ์ถ”๊ฐ€
76
+
77
  ## ๋‚จ์€ ์ž‘์—…
78
+ - [ ] ๋กœ์ปฌPC ์žฅ์น˜ ์กฐํšŒ Python ์Šคํฌ๋ฆฝํŠธ ๊ฐœ๋ฐœ
79
+ - [ ] LocalPCAgent ์„œ๋ฒ„์— ์žฅ์น˜ ์กฐํšŒ ๊ธฐ๋Šฅ ํ†ตํ•ฉ
80
+ - [ ] Flask ๋ฐฑ์—”๋“œ ์žฅ์น˜ ์กฐํšŒ API ๊ตฌํ˜„
81
+ - [ ] ํ”„๋ก ํŠธ์—”๋“œ UI ํ™•์žฅ
82
  - [ ] ์ „์ฒด ์‹œ์Šคํ…œ ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…
83
+ - [ ] ์ตœ์ข… ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ ๋ฌธ์„œํ™” ๋ณด์™„
 
84
 
85
  ## ํŒŒ์ผ ๊ตฌ์กฐ
86
  - `app_revised.py`: ๋ฉ”์ธ Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
 
107
  - `docs/`: ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ
108
  - `project_plan.md`: ํ”„๋กœ์ ํŠธ ๊ณ„ํš ๋ฌธ์„œ
109
  - `cleanup_plan.md`: ์ฝ”๋“œ ์ •๋ฆฌ ๊ณ„ํš ๋ฌธ์„œ
110
+ - **์‹ ๊ทœ ํŒŒ์ผ:**
111
+ - `LocalPCAgent/programs/scan_devices.py`: ์žฅ์น˜ ์กฐํšŒ Python ์Šคํฌ๋ฆฝํŠธ
112
 
113
  ## LocalPCAgent ํ†ตํ•ฉ ์š”์•ฝ
114
  - **๋ฐฑ์—”๋“œ API**: `app/app_device_routes.py`์— ๊ตฌํ˜„
 
118
  - `/api/device/programs`: ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ
119
  - `/api/device/programs/<program_id>/execute`: ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
120
  - `/api/device/execute-custom`: ์‚ฌ์šฉ์ž ์ •์˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
121
+ - **์ถ”๊ฐ€ ์˜ˆ์ •:** `/api/device/scan-ports`: COM ํฌํŠธ ๋ฐ USB ์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ
122
 
123
  - **ํ”„๋ŸฐํŠธ์—”๋“œ**: ๊ธฐ๋Šฅ ์™„์„ฑ
124
  - ์žฅ์น˜ ์ œ์–ด ํƒญ UI: `templates/index.html`์— ๊ตฌํ˜„