jeongsoo commited on
Commit
2c2f318
Β·
1 Parent(s): 64371be
app/app_device_routes.py CHANGED
@@ -170,7 +170,7 @@ def register_device_routes(app, login_required, DEVICE_SERVER_URL):
170
  }), 502 # Bad Gateway
171
 
172
  except requests.exceptions.Timeout:
173
- logger.error(f"μž₯치 μ„œλ²„ μ—°κ²° νƒ€μž„μ•„μ›ƒ ({DEVICE_SERVER_URL})")
174
  return jsonify({
175
  "success": False,
176
  "error": "μž₯치 μ„œλ²„ μ—°κ²° νƒ€μž„μ•„μ›ƒ. μ„œλ²„ 응닡이 λ„ˆλ¬΄ λŠλ¦½λ‹ˆλ‹€."
@@ -375,4 +375,88 @@ def register_device_routes(app, login_required, DEVICE_SERVER_URL):
375
  return jsonify({
376
  "success": False,
377
  "error": f"ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {str(e)}"
378
- }), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  }), 502 # Bad Gateway
171
 
172
  except requests.exceptions.Timeout:
173
+ logger.error(f"μž₯치 μ„œλ²„ μ—°κ²° νƒ€μž„μ•„μ›ƒ ({get_device_url()})")
174
  return jsonify({
175
  "success": False,
176
  "error": "μž₯치 μ„œλ²„ μ—°κ²° νƒ€μž„μ•„μ›ƒ. μ„œλ²„ 응닡이 λ„ˆλ¬΄ λŠλ¦½λ‹ˆλ‹€."
 
375
  return jsonify({
376
  "success": False,
377
  "error": f"ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {str(e)}"
378
+ }), 500
379
+
380
+
381
+ @app.route('/api/device/execute-custom', methods=['POST'])
382
+ @login_required
383
+ def execute_custom_program():
384
+ """μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ API"""
385
+ logger.info("μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­")
386
+
387
+ try:
388
+ # μš”μ²­ 데이터 확인
389
+ request_data = request.get_json()
390
+ if not request_data or 'command' not in request_data:
391
+ logger.error("λͺ…λ Ήμ–΄κ°€ μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
392
+ return jsonify({
393
+ "success": False,
394
+ "error": "μ‹€ν–‰ν•  λͺ…λ Ήμ–΄λ₯Ό μ œκ³΅ν•΄μ£Όμ„Έμš”."
395
+ }), 400 # Bad Request
396
+
397
+ command = request_data['command'].strip()
398
+ if not command:
399
+ logger.error("λͺ…λ Ήμ–΄κ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
400
+ return jsonify({
401
+ "success": False,
402
+ "error": "μ‹€ν–‰ν•  λͺ…λ Ήμ–΄λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."
403
+ }), 400 # Bad Request
404
+
405
+ # ν˜„μž¬ μž₯치 μ„œλ²„ URL κ°€μ Έμ˜€κΈ°
406
+ current_device_url = get_device_url()
407
+ api_path = "/api/execute-custom" # LocalPCAgent에 ν•΄λ‹Ή API μ—”λ“œν¬μΈνŠΈκ°€ μžˆμ–΄μ•Ό 함
408
+
409
+ logger.info(f"μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­: {current_device_url}{api_path}, λͺ…λ Ήμ–΄: {command}")
410
+ response = requests.post(
411
+ f"{current_device_url}{api_path}",
412
+ json={"command": command},
413
+ timeout=10 # ν”„λ‘œκ·Έλž¨ μ‹€ν–‰μ—λŠ” 더 κΈ΄ μ‹œκ°„ λΆ€μ—¬
414
+ )
415
+
416
+ logger.debug(f"μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 응닡 μƒνƒœ μ½”λ“œ: {response.status_code}")
417
+
418
+ if response.status_code == 200:
419
+ try:
420
+ data = response.json()
421
+ success = data.get("success", False)
422
+ message = data.get("message", "")
423
+ logger.info(f"μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 응닡: {success}, {message}")
424
+ return jsonify(data) # μ„œλ²„ 응닡 κ·ΈλŒ€λ‘œ λ°˜ν™˜
425
+ except requests.exceptions.JSONDecodeError:
426
+ logger.error("μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 응닡 JSON νŒŒμ‹± μ‹€νŒ¨")
427
+ return jsonify({
428
+ "success": False,
429
+ "error": "μ„œλ²„λ‘œλΆ€ν„° μœ νš¨ν•˜μ§€ μ•Šμ€ JSON 응닡"
430
+ }), 502
431
+ else:
432
+ error_message = f"μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­ μ‹€νŒ¨: {response.status_code}"
433
+ try:
434
+ error_message += f" - {response.json().get('error', response.text)}"
435
+ except Exception:
436
+ error_message += f" - {response.text}"
437
+ logger.warning(error_message)
438
+ return jsonify({
439
+ "success": False,
440
+ "error": error_message
441
+ }), 502
442
+
443
+ except requests.exceptions.Timeout:
444
+ logger.error("μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­ μ‹œκ°„ 초과")
445
+ return jsonify({
446
+ "success": False,
447
+ "error": "ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­ μ‹œκ°„μ΄ μ΄ˆκ³Όλ˜μ—ˆμŠ΅λ‹ˆλ‹€."
448
+ }), 504
449
+
450
+ except requests.exceptions.ConnectionError:
451
+ logger.error("μž₯치 관리 μ„œλ²„ μ—°κ²° μ‹€νŒ¨")
452
+ return jsonify({
453
+ "success": False,
454
+ "error": "μž₯치 관리 μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€. μ„œλ²„κ°€ μ‹€ν–‰ 쀑인지 ν™•μΈν•΄μ£Όμ„Έμš”."
455
+ }), 503
456
+
457
+ except Exception as e:
458
+ logger.error(f"μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
459
+ return jsonify({
460
+ "success": False,
461
+ "error": f"ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {str(e)}"
462
+ }), 500
app/static/js/app-device.js CHANGED
@@ -9,72 +9,88 @@ const DeviceControl = {
9
  isStatusChecked: false,
10
  isLoadingPrograms: false,
11
  programsList: [],
12
-
13
  // DOM μš”μ†Œλ“€
14
  elements: {
15
  // νƒ­ 및 μ„Ήμ…˜
16
  deviceTab: null,
17
  deviceSection: null,
18
-
19
  // μ—°κ²° κ΄€λ ¨
20
  deviceServerUrlInput: null,
21
  connectDeviceServerBtn: null,
22
  deviceConnectionStatus: null,
23
-
24
  // κΈ°λ³Έ κΈ°λŠ₯
25
  deviceBasicFunctions: null,
26
  checkDeviceStatusBtn: null,
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
  // λͺ¨λ“ˆ μ΄ˆκΈ°ν™”
39
  init: function() {
40
  console.log('μž₯치 μ œμ–΄ λͺ¨λ“ˆ μ΄ˆκΈ°ν™” 쀑...');
41
-
42
  // DOM μš”μ†Œ μ°Έμ‘° κ°€μ Έμ˜€κΈ°
43
  this.initElements();
44
-
45
  // 이벀트 λ¦¬μŠ€λ„ˆ 등둝
46
  this.initEventListeners();
47
-
48
  console.log('μž₯치 μ œμ–΄ λͺ¨λ“ˆ μ΄ˆκΈ°ν™” μ™„λ£Œ');
49
  },
50
-
51
  // DOM μš”μ†Œ μ°Έμ‘° μ΄ˆκΈ°ν™”
52
  initElements: function() {
53
  // νƒ­ 및 μ„Ήμ…˜
54
  this.elements.deviceTab = document.getElementById('deviceTab');
55
  this.elements.deviceSection = document.getElementById('deviceSection');
56
-
57
  // μ—°κ²° κ΄€λ ¨
58
  this.elements.deviceServerUrlInput = document.getElementById('deviceServerUrlInput');
59
  this.elements.connectDeviceServerBtn = document.getElementById('connectDeviceServerBtn');
60
  this.elements.deviceConnectionStatus = document.getElementById('deviceConnectionStatus');
61
-
62
  // κΈ°λ³Έ κΈ°λŠ₯
63
  this.elements.deviceBasicFunctions = document.getElementById('deviceBasicFunctions');
64
  this.elements.checkDeviceStatusBtn = document.getElementById('checkDeviceStatusBtn');
65
  this.elements.deviceStatusResult = document.getElementById('deviceStatusResult');
66
-
67
- // ν”„λ‘œκ·Έλž¨ μ‹€ν–‰
68
  this.elements.deviceProgramControl = document.getElementById('deviceProgramControl');
69
  this.elements.getProgramsBtn = document.getElementById('getProgramsBtn');
70
  this.elements.programsList = document.getElementById('programsList');
71
  this.elements.programSelectDropdown = document.getElementById('programSelectDropdown');
72
  this.elements.executeProgramBtn = document.getElementById('executeProgramBtn');
73
  this.elements.executeResult = document.getElementById('executeResult');
74
-
 
 
 
 
 
 
 
 
75
  console.log('μž₯치 μ œμ–΄ DOM μš”μ†Œ μ°Έμ‘° μ΄ˆκΈ°ν™” μ™„λ£Œ');
76
  },
77
-
78
  // 이벀트 λ¦¬μŠ€λ„ˆ 등둝
79
  initEventListeners: function() {
80
  // νƒ­ μ „ν™˜
@@ -84,7 +100,7 @@ const DeviceControl = {
84
  this.switchToDeviceTab();
85
  });
86
  }
87
-
88
  // μ„œλ²„ μ—°κ²°
89
  if (this.elements.connectDeviceServerBtn) {
90
  this.elements.connectDeviceServerBtn.addEventListener('click', () => {
@@ -92,7 +108,7 @@ const DeviceControl = {
92
  this.connectServer();
93
  });
94
  }
95
-
96
  // μ—”ν„° ν‚€λ‘œ μ—°κ²°
97
  if (this.elements.deviceServerUrlInput) {
98
  this.elements.deviceServerUrlInput.addEventListener('keydown', (event) => {
@@ -103,7 +119,7 @@ const DeviceControl = {
103
  }
104
  });
105
  }
106
-
107
  // μž₯치 μƒνƒœ 확인
108
  if (this.elements.checkDeviceStatusBtn) {
109
  this.elements.checkDeviceStatusBtn.addEventListener('click', () => {
@@ -111,7 +127,7 @@ const DeviceControl = {
111
  this.checkDeviceStatus();
112
  });
113
  }
114
-
115
  // ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 쑰회
116
  if (this.elements.getProgramsBtn) {
117
  this.elements.getProgramsBtn.addEventListener('click', () => {
@@ -119,7 +135,7 @@ const DeviceControl = {
119
  this.loadProgramsList();
120
  });
121
  }
122
-
123
  // ν”„λ‘œκ·Έλž¨ 선택 λ³€κ²½
124
  if (this.elements.programSelectDropdown) {
125
  this.elements.programSelectDropdown.addEventListener('change', (event) => {
@@ -127,76 +143,102 @@ const DeviceControl = {
127
  this.updateExecuteButton();
128
  });
129
  }
130
-
131
- // ν”„λ‘œκ·Έλž¨ μ‹€ν–‰
132
  if (this.elements.executeProgramBtn) {
133
  this.elements.executeProgramBtn.addEventListener('click', () => {
134
  const programId = this.elements.programSelectDropdown.value;
135
- console.log(`ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ λ²„νŠΌ 클릭, μ„ νƒλœ ID: ${programId}`);
136
  this.executeProgram(programId);
137
  });
138
  }
139
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  console.log('μž₯치 μ œμ–΄ 이벀트 λ¦¬μŠ€λ„ˆ 등둝 μ™„λ£Œ');
141
  },
142
-
143
  // μž₯치 μ œμ–΄ νƒ­μœΌλ‘œ μ „ν™˜
144
  switchToDeviceTab: function() {
145
  // λͺ¨λ“  νƒ­κ³Ό νƒ­ μ½˜ν…μΈ  λΉ„ν™œμ„±ν™”
146
  const tabs = document.querySelectorAll('.tab');
147
  const tabContents = document.querySelectorAll('.tab-content');
148
-
149
  tabs.forEach(tab => tab.classList.remove('active'));
150
  tabContents.forEach(content => content.classList.remove('active'));
151
-
152
  // μž₯치 μ œμ–΄ νƒ­ ν™œμ„±ν™”
153
  this.elements.deviceTab.classList.add('active');
154
  this.elements.deviceSection.classList.add('active');
155
-
156
  console.log('μž₯치 μ œμ–΄ νƒ­μœΌλ‘œ μ „ν™˜ μ™„λ£Œ');
157
  },
158
-
159
  // μ„œλ²„ μ—°κ²° ν•¨μˆ˜
160
  connectServer: async function() {
161
  // URL κ°€μ Έμ˜€κΈ° (μž…λ ₯된 것이 있으면 λ°±μ—…μœΌλ‘œ μ‚¬μš©)
162
  const inputUrl = this.elements.deviceServerUrlInput.value.trim();
163
-
164
  // μ—°κ²° μ‹œλ„ 쀑 UI μ—…λ°μ΄νŠΈ
165
  this.elements.connectDeviceServerBtn.disabled = true;
166
  this.updateConnectionStatus('connecting', 'ν™˜κ²½λ³€μˆ˜μ— μ €μž₯된 μ„œλ²„λ‘œ μ—°κ²° μ‹œλ„ 쀑...');
167
-
168
  try {
169
  console.log('ν™˜κ²½λ³€μˆ˜μ— μ €μž₯된 μž₯치 μ„œλ²„λ‘œ μ—°κ²° μ‹œλ„');
170
-
171
  // λ°±μ—”λ“œ API ν˜ΈμΆœν•˜μ—¬ μ„œλ²„ μƒνƒœ 확인
172
  const response = await AppUtils.fetchWithTimeout('/api/device/status', {
173
  method: 'GET'
174
  }, 10000); // 10초 νƒ€μž„μ•„μ›ƒ
175
-
176
  const data = await response.json();
177
-
178
  if (response.ok && data.success) {
179
  // μ—°κ²° 성곡
180
  console.log('ν™˜κ²½λ³€μˆ˜ μ„€μ • μž₯치 μ„œλ²„ μ—°κ²° 성곡:', data);
181
  this.isConnected = true;
182
  this.updateConnectionStatus('connected', `μ„œλ²„ μ—°κ²° 성곡! μƒνƒœ: ${data.server_status || '정상'}`);
183
-
184
  // κΈ°λŠ₯ UI ν™œμ„±ν™”
185
- this.elements.deviceBasicFunctions.classList.add('active');
186
- this.elements.deviceProgramControl.classList.add('active');
187
-
 
 
 
188
  // μž₯치 μƒνƒœ μžλ™ 체크
189
  this.checkDeviceStatus();
190
-
191
  // ν”„λ‘œκ·Έλž¨ λͺ©οΏ½οΏ½ μžλ™ λ‘œλ“œ
192
  this.loadProgramsList();
193
-
194
  // μ‹œμŠ€ν…œ μ•Œλ¦Ό
195
  AppUtils.addSystemNotification(`μž₯치 관리 μ„œλ²„ μ—°κ²° 성곡! (ν™˜κ²½λ³€μˆ˜ URL)`);
196
  } else {
197
  // ν™˜κ²½λ³€μˆ˜ URL μ—°κ²° μ‹€νŒ¨, μž…λ ₯된 URL둜 μ‹œλ„
198
  console.warn('ν™˜κ²½λ³€μˆ˜ μ„€μ • μž₯치 μ„œλ²„ μ—°κ²° μ‹€νŒ¨, μž…λ ₯ URL둜 μž¬μ‹œλ„ν•©λ‹ˆλ‹€:', data);
199
-
200
  // μž…λ ₯ URL이 μžˆλŠ”μ§€ 확인
201
  if (!inputUrl) {
202
  console.error('μž…λ ₯된 URL이 μ—†μ–΄ μ—°κ²° μ‹€νŒ¨');
@@ -204,11 +246,11 @@ const DeviceControl = {
204
  this.updateConnectionStatus('error', 'ν™˜κ²½λ³€μˆ˜ URL μ—°κ²° μ‹€νŒ¨ 및 μž…λ ₯된 URL이 μ—†μŠ΅λ‹ˆλ‹€. URL을 μž…λ ₯ν•΄μ£Όμ„Έμš”.');
205
  return;
206
  }
207
-
208
  // μž…λ ₯ URL둜 μž¬μ‹œλ„
209
  this.updateConnectionStatus('connecting', `μž…λ ₯ URL(${inputUrl})둜 μ—°κ²° μ‹œλ„ 쀑...`);
210
  console.log(`μž…λ ₯ν•œ URL둜 μž₯치 μ„œλ²„ μ—°κ²° μ‹œλ„: ${inputUrl}`);
211
-
212
  // λ°±μ—”λ“œ API 호좜 - μž…λ ₯ URL μ‚¬μš©
213
  const customUrlResponse = await AppUtils.fetchWithTimeout('/api/device/connect', {
214
  method: 'POST',
@@ -217,25 +259,29 @@ const DeviceControl = {
217
  },
218
  body: JSON.stringify({ url: inputUrl })
219
  }, 10000);
220
-
221
  const customUrlData = await customUrlResponse.json();
222
-
223
  if (customUrlResponse.ok && customUrlData.success) {
224
  // μž…λ ₯ URL μ—°κ²° 성곡
225
  console.log('μž…λ ₯ URL μž₯치 μ„œλ²„ μ—°κ²° 성곡:', customUrlData);
226
  this.isConnected = true;
227
  this.updateConnectionStatus('connected', `μ„œλ²„ μ—°κ²° 성곡! μƒνƒœ: ${customUrlData.server_status || '정상'}`);
228
-
229
  // κΈ°λŠ₯ UI ν™œμ„±ν™”
230
- this.elements.deviceBasicFunctions.classList.add('active');
231
- this.elements.deviceProgramControl.classList.add('active');
232
-
 
 
 
 
233
  // μž₯치 μƒνƒœ μžλ™ 체크
234
  this.checkDeviceStatus();
235
-
236
  // ν”„λ‘œκ·Έλž¨ λͺ©λ‘ μžλ™ λ‘œλ“œ
237
  this.loadProgramsList();
238
-
239
  // μ‹œμŠ€ν…œ μ•Œλ¦Ό
240
  AppUtils.addSystemNotification(`μž₯치 관리 μ„œλ²„ μ—°κ²° 성곡! (${inputUrl})`);
241
  } else {
@@ -249,16 +295,16 @@ const DeviceControl = {
249
  // μ˜ˆμ™Έ λ°œμƒ
250
  console.error('μ„œλ²„ μ—°κ²° 쀑 였λ₯˜ λ°œμƒ:', error);
251
  this.isConnected = false;
252
-
253
  // ν™˜κ²½λ³€μˆ˜ URL μ—°κ²° μ‹€νŒ¨, μž…λ ₯된 URL둜 μ‹œλ„
254
  if (inputUrl) {
255
  console.warn('ν™˜κ²½λ³€μˆ˜ URL μ—°κ²° μ‹œ 였λ₯˜ λ°œμƒ, μž…λ ₯ URL둜 μž¬μ‹œλ„ν•©λ‹ˆλ‹€');
256
-
257
  try {
258
  // μž…λ ₯ URL둜 μž¬μ‹œλ„
259
  this.updateConnectionStatus('connecting', `μž…λ ₯ URL(${inputUrl})둜 μ—°κ²° μ‹œλ„ 쀑...`);
260
  console.log(`μž…λ ₯ν•œ URL둜 μž₯치 μ„œλ²„ μ—°κ²° μ‹œλ„: ${inputUrl}`);
261
-
262
  // λ°±μ—”λ“œ API 호좜 - μž…λ ₯ URL μ‚¬μš©
263
  const customUrlResponse = await AppUtils.fetchWithTimeout('/api/device/connect', {
264
  method: 'POST',
@@ -267,25 +313,29 @@ const DeviceControl = {
267
  },
268
  body: JSON.stringify({ url: inputUrl })
269
  }, 10000);
270
-
271
  const customUrlData = await customUrlResponse.json();
272
-
273
  if (customUrlResponse.ok && customUrlData.success) {
274
  // μž…λ ₯ URL μ—°κ²° 성곡
275
  console.log('μž…λ ₯ URL μž₯치 μ„œλ²„ μ—°κ²° 성곡:', customUrlData);
276
  this.isConnected = true;
277
  this.updateConnectionStatus('connected', `μ„œλ²„ μ—°κ²° 성곡! μƒνƒœ: ${customUrlData.server_status || '정상'}`);
278
-
279
  // κΈ°λŠ₯ UI ν™œμ„±ν™”
280
- this.elements.deviceBasicFunctions.classList.add('active');
281
- this.elements.deviceProgramControl.classList.add('active');
282
-
 
 
 
 
283
  // μž₯치 μƒνƒœ μžλ™ 체크
284
  this.checkDeviceStatus();
285
-
286
  // ν”„λ‘œκ·Έλž¨ λͺ©λ‘ μžλ™ λ‘œλ“œ
287
  this.loadProgramsList();
288
-
289
  // μ‹œμŠ€ν…œ μ•Œλ¦Ό
290
  AppUtils.addSystemNotification(`μž₯치 관리 μ„œλ²„ μ—°κ²° 성곡! (${inputUrl})`);
291
  return; // μ„±κ³΅ν•˜λ©΄ μ—¬κΈ°μ„œ μ’…λ£Œ
@@ -297,7 +347,7 @@ const DeviceControl = {
297
  } catch (inputUrlError) {
298
  // μž…λ ₯ URL둜 μž¬μ‹œλ„ 쀑 였λ₯˜
299
  console.error('μž…λ ₯ URL둜 μž¬μ‹œλ„ 쀑 였λ₯˜ λ°œμƒ:', inputUrlError);
300
-
301
  if (inputUrlError.message.includes('μ‹œκ°„μ΄ 초과')) {
302
  this.updateConnectionStatus('error', 'μ„œλ²„ μ—°κ²° μ‹œκ°„ 초과. μ„œλ²„κ°€ μ‹€ν–‰ 쀑인지 ν™•μΈν•΄μ£Όμ„Έμš”.');
303
  } else {
@@ -317,23 +367,24 @@ const DeviceControl = {
317
  this.elements.connectDeviceServerBtn.disabled = false;
318
  }
319
  },
320
-
321
  // μ—°κ²° μƒνƒœ μ—…λ°μ΄νŠΈ
322
  updateConnectionStatus: function(status, message) {
323
  const statusElement = this.elements.deviceConnectionStatus;
324
-
 
325
  // λͺ¨λ“  μƒνƒœ 클래슀 제거
326
  statusElement.classList.remove('connected', 'disconnected', 'error', 'connecting');
327
-
328
  // μƒνƒœμ— 따라 클래슀 μΆ”κ°€
329
  statusElement.classList.add(status);
330
-
331
  // λ©”μ‹œμ§€ μ—…λ°μ΄νŠΈ
332
  statusElement.textContent = message;
333
-
334
  console.log(`μ—°κ²° μƒνƒœ μ—…λ°μ΄νŠΈ: ${status} - ${message}`);
335
  },
336
-
337
  // μž₯치 μƒνƒœ 확인
338
  checkDeviceStatus: async function() {
339
  if (!this.isConnected) {
@@ -341,26 +392,27 @@ const DeviceControl = {
341
  console.error('μž₯치 μƒνƒœ 확인 μ‹œλ„ 쀑 였λ₯˜: μ„œλ²„ μ—°κ²° μ•ˆλ¨');
342
  return;
343
  }
344
-
345
  // μƒνƒœ 확인 쀑 UI μ—…λ°μ΄νŠΈ
346
  this.elements.checkDeviceStatusBtn.disabled = true;
347
  this.elements.deviceStatusResult.value = 'μž₯치 μƒνƒœ 확인 쀑...';
348
-
349
  try {
350
  console.log('μž₯치 μƒνƒœ 확인 μš”μ²­ 전솑');
351
-
352
  // λ°±μ—”λ“œ API 호좜
353
  const response = await AppUtils.fetchWithTimeout('/api/device/status', {
354
  method: 'GET'
355
  });
356
-
357
  const data = await response.json();
358
-
359
  if (response.ok && data.success) {
360
  // μƒνƒœ 확인 성곡
361
  console.log('μž₯치 μƒνƒœ 확인 성곡:', data);
362
  this.isStatusChecked = true;
363
- this.elements.deviceStatusResult.value = JSON.stringify(data, null, 2);
 
364
  } else {
365
  // μƒνƒœ 확인 μ‹€νŒ¨
366
  console.error('μž₯치 μƒνƒœ 확인 μ‹€νŒ¨:', data);
@@ -375,7 +427,7 @@ const DeviceControl = {
375
  this.elements.checkDeviceStatusBtn.disabled = false;
376
  }
377
  },
378
-
379
  // ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 쑰회
380
  loadProgramsList: async function() {
381
  if (!this.isConnected) {
@@ -383,43 +435,46 @@ const DeviceControl = {
383
  console.error('ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 쑰회 μ‹œλ„ 쀑 였λ₯˜: μ„œλ²„ μ—°κ²° μ•ˆλ¨');
384
  return;
385
  }
386
-
387
  // 이미 λ‘œλ”© 쀑이면 쀑볡 μš”μ²­ λ°©μ§€
388
  if (this.isLoadingPrograms) {
389
  console.log('이미 ν”„λ‘œκ·Έλž¨ λͺ©λ‘ λ‘œλ”© 쀑');
390
  return;
391
  }
392
-
393
  // λ‘œλ”© 쀑 UI μ—…λ°μ΄νŠΈ
394
  this.isLoadingPrograms = true;
395
- this.elements.getProgramsBtn.disabled = true;
396
- this.elements.programsList.innerHTML = `
397
- <div class="loading-message">
398
- ${AppUtils.createLoadingSpinner()} ν”„λ‘œκ·Έλž¨ λͺ©λ‘ λ‘œλ“œ 쀑...
399
- </div>
400
- `;
401
-
 
 
 
402
  try {
403
  console.log('ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 쑰회 μš”μ²­ 전솑');
404
-
405
  // λ°±μ—”λ“œ API 호좜
406
  const response = await AppUtils.fetchWithTimeout('/api/device/programs', {
407
  method: 'GET'
408
  });
409
-
410
  const data = await response.json();
411
-
412
  if (response.ok && data.success) {
413
  // λͺ©λ‘ 쑰회 성곡
414
  console.log('ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 쑰회 성곡:', data);
415
  this.programsList = data.programs || [];
416
-
417
  // λͺ©λ‘ ν‘œμ‹œ
418
  this.displayProgramsList();
419
-
420
  // λ“œλ‘­λ‹€μš΄ μ—…λ°μ΄νŠΈ
421
  this.updateProgramsDropdown();
422
-
423
  // μ‹€ν–‰ λ²„νŠΌ μƒνƒœ μ—…λ°μ΄νŠΈ
424
  this.updateExecuteButton();
425
  } else {
@@ -434,14 +489,15 @@ const DeviceControl = {
434
  } finally {
435
  // λ‘œλ”© μƒνƒœ 및 λ²„νŠΌ μƒνƒœ 볡원
436
  this.isLoadingPrograms = false;
437
- this.elements.getProgramsBtn.disabled = false;
438
  }
439
  },
440
-
441
  // ν”„λ‘œκ·Έλž¨ λͺ©λ‘ ν‘œμ‹œ
442
  displayProgramsList: function() {
443
  const programsListElement = this.elements.programsList;
444
-
 
445
  if (!this.programsList || this.programsList.length === 0) {
446
  programsListElement.innerHTML = `
447
  <div class="no-programs-message">
@@ -450,7 +506,7 @@ const DeviceControl = {
450
  `;
451
  return;
452
  }
453
-
454
  // ν…Œμ΄λΈ” ν˜•νƒœλ‘œ ν”„λ‘œκ·Έλž¨ λͺ©λ‘ ν‘œμ‹œ
455
  let html = `
456
  <table class="program-list">
@@ -463,7 +519,7 @@ const DeviceControl = {
463
  </thead>
464
  <tbody>
465
  `;
466
-
467
  // ν”„λ‘œκ·Έλž¨ ν•­λͺ© 생성
468
  this.programsList.forEach(program => {
469
  html += `
@@ -474,7 +530,7 @@ const DeviceControl = {
474
  </tr>
475
  `;
476
  });
477
-
478
  html += `
479
  </tbody>
480
  </table>
@@ -482,17 +538,18 @@ const DeviceControl = {
482
  총 ${this.programsList.length}개 ν”„λ‘œκ·Έλž¨
483
  </div>
484
  `;
485
-
486
  programsListElement.innerHTML = html;
487
  },
488
-
489
  // ν”„λ‘œκ·Έλž¨ λ“œλ‘­λ‹€μš΄ μ—…λ°μ΄νŠΈ
490
  updateProgramsDropdown: function() {
491
  const dropdown = this.elements.programSelectDropdown;
492
-
 
493
  // κΈ°μ‘΄ μ˜΅μ…˜ 제거
494
  dropdown.innerHTML = '';
495
-
496
  // κΈ°λ³Έ μ˜΅μ…˜ μΆ”κ°€
497
  const defaultOption = document.createElement('option');
498
  defaultOption.value = '';
@@ -500,52 +557,53 @@ const DeviceControl = {
500
  ? '-- μ‹€ν–‰ν•  ν”„λ‘œκ·Έλž¨ 선택 --'
501
  : '-- ν”„λ‘œκ·Έλž¨ μ—†μŒ --';
502
  dropdown.appendChild(defaultOption);
503
-
504
  // ν”„λ‘œκ·Έλž¨ μ˜΅μ…˜ μΆ”κ°€
505
  this.programsList.forEach(program => {
506
  const option = document.createElement('option');
507
  option.value = program.id || '';
508
  option.textContent = program.name || 'μ•Œ 수 μ—†μŒ';
509
-
510
  // μ„€λͺ…이 있으면 κ΄„ν˜Έλ‘œ μΆ”κ°€
511
  if (program.description) {
512
  option.textContent += ` (${program.description})`;
513
  }
514
-
515
  dropdown.appendChild(option);
516
  });
517
  },
518
-
519
  // μ‹€ν–‰ λ²„νŠΌ μƒνƒœ μ—…λ°μ΄νŠΈ
520
  updateExecuteButton: function() {
521
  const dropdown = this.elements.programSelectDropdown;
522
  const executeBtn = this.elements.executeProgramBtn;
523
-
 
524
  // μ„ νƒλœ ν”„λ‘œκ·Έλž¨μ΄ μžˆμ„ λ•Œλ§Œ λ²„νŠΌ ν™œμ„±ν™”
525
  executeBtn.disabled = !dropdown.value;
526
  },
527
-
528
- // ν”„λ‘œκ·Έλž¨ μ‹€ν–‰
529
  executeProgram: async function(programId) {
530
  if (!this.isConnected) {
531
  this.showExecuteResult('error', '였λ₯˜: λ¨Όμ € μ„œλ²„μ— μ—°κ²°ν•΄μ•Ό ν•©λ‹ˆλ‹€.');
532
  console.error('ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μ‹œλ„ 쀑 였λ₯˜: μ„œλ²„ μ—°κ²° μ•ˆλ¨');
533
  return;
534
  }
535
-
536
  if (!programId) {
537
  this.showExecuteResult('error', '였λ₯˜: μ‹€ν–‰ν•  ν”„λ‘œκ·Έλž¨μ„ μ„ νƒν•΄μ£Όμ„Έμš”.');
538
  console.error('ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μ‹œλ„ 쀑 였λ₯˜: ν”„λ‘œκ·Έλž¨ ID μ—†μŒ');
539
  return;
540
  }
541
-
542
  // μ‹€ν–‰ 쀑 UI μ—…λ°μ΄νŠΈ
543
- this.elements.executeProgramBtn.disabled = true;
544
  this.showExecuteResult('loading', 'ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 쀑...');
545
-
546
  try {
547
  console.log(`ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­ 전솑: ${programId}`);
548
-
549
  // λ°±μ—”λ“œ API 호좜
550
  const response = await AppUtils.fetchWithTimeout(`/api/device/programs/${programId}/execute`, {
551
  method: 'POST',
@@ -554,14 +612,14 @@ const DeviceControl = {
554
  },
555
  body: JSON.stringify({})
556
  }, 15000); // 15초 νƒ€μž„μ•„μ›ƒ (싀행에 μ‹œκ°„μ΄ 더 걸릴 수 있음)
557
-
558
  const data = await response.json();
559
-
560
  if (response.ok && data.success) {
561
  // μ‹€ν–‰ 성곡
562
  console.log('ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 성곡:', data);
563
  this.showExecuteResult('success', `μ‹€ν–‰ 성곡: ${data.message || 'ν”„λ‘œκ·Έλž¨μ΄ μ„±κ³΅μ μœΌλ‘œ μ‹€ν–‰λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'}`);
564
-
565
  // μ‹œμŠ€ν…œ μ•Œλ¦Ό
566
  AppUtils.addSystemNotification(`ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 성곡: ${this.getSelectedProgramName()}`);
567
  } else {
@@ -572,7 +630,7 @@ const DeviceControl = {
572
  } catch (error) {
573
  // μ˜ˆμ™Έ λ°œμƒ
574
  console.error('ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ:', error);
575
-
576
  if (error.message.includes('μ‹œκ°„μ΄ 초과')) {
577
  this.showExecuteResult('error', 'ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­ μ‹œκ°„ 초과. μ„œλ²„ 응닡이 μ—†μŠ΅λ‹ˆλ‹€.');
578
  } else {
@@ -580,20 +638,24 @@ const DeviceControl = {
580
  }
581
  } finally {
582
  // λ²„νŠΌ λ‹€μ‹œ ν™œμ„±ν™”
583
- this.elements.executeProgramBtn.disabled = false;
584
  }
585
  },
586
-
587
  // μ„ νƒλœ ν”„λ‘œκ·Έλž¨ 이름 κ°€μ Έμ˜€κΈ°
588
  getSelectedProgramName: function() {
589
  const dropdown = this.elements.programSelectDropdown;
 
590
  const selectedOption = dropdown.options[dropdown.selectedIndex];
591
  return selectedOption ? selectedOption.textContent : 'μ•Œ 수 μ—†λŠ” ν”„λ‘œκ·Έλž¨';
592
  },
593
-
594
  // ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 였λ₯˜ ν‘œμ‹œ
595
  showProgramsError: function(errorMessage) {
596
- this.elements.programsList.innerHTML = `
 
 
 
597
  <div class="error-message">
598
  <i class="fas fa-exclamation-circle"></i> ${errorMessage}
599
  <button class="retry-button" id="retryLoadProgramsBtn">
@@ -601,24 +663,31 @@ const DeviceControl = {
601
  </button>
602
  </div>
603
  `;
604
-
605
  // μž¬μ‹œλ„ λ²„νŠΌ 이벀트 λ¦¬μŠ€λ„ˆ
606
- document.getElementById('retryLoadProgramsBtn').addEventListener('click', () => {
607
- console.log('ν”„λ‘œκ·Έλž¨ λͺ©λ‘ μž¬μ‹œλ„ λ²„νŠΌ 클릭');
608
- this.loadProgramsList();
609
- });
 
 
 
 
 
 
610
  },
611
-
612
- // μ‹€ν–‰ κ²°κ³Ό ν‘œμ‹œ
613
  showExecuteResult: function(status, message) {
614
  const resultElement = this.elements.executeResult;
615
-
 
616
  // λͺ¨λ“  μƒνƒœ 클래슀 제거
617
  resultElement.classList.remove('success', 'error', 'warning');
618
-
619
  // λ‚΄μš© μ΄ˆκΈ°ν™”
620
  resultElement.innerHTML = '';
621
-
622
  // μƒνƒœμ— 따라 처리
623
  switch (status) {
624
  case 'success':
@@ -639,15 +708,136 @@ const DeviceControl = {
639
  default:
640
  resultElement.textContent = message;
641
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  }
 
643
  };
644
 
645
  // νŽ˜μ΄μ§€ λ‘œλ“œ μ™„λ£Œ μ‹œ λͺ¨λ“ˆ μ΄ˆκΈ°ν™”
646
  document.addEventListener('DOMContentLoaded', function() {
647
  console.log('μž₯치 μ œμ–΄ λͺ¨λ“ˆ λ‘œλ“œλ¨');
648
-
649
  // DOM이 μ™„μ „νžˆ λ‘œλ“œλœ ν›„ μ•½κ°„μ˜ 지연을 두고 μ΄ˆκΈ°ν™”
 
650
  setTimeout(() => {
 
 
 
 
 
 
 
651
  DeviceControl.init();
652
  }, 100);
653
- });
 
9
  isStatusChecked: false,
10
  isLoadingPrograms: false,
11
  programsList: [],
12
+
13
  // DOM μš”μ†Œλ“€
14
  elements: {
15
  // νƒ­ 및 μ„Ήμ…˜
16
  deviceTab: null,
17
  deviceSection: null,
18
+
19
  // μ—°κ²° κ΄€λ ¨
20
  deviceServerUrlInput: null,
21
  connectDeviceServerBtn: null,
22
  deviceConnectionStatus: null,
23
+
24
  // κΈ°λ³Έ κΈ°λŠ₯
25
  deviceBasicFunctions: null,
26
  checkDeviceStatusBtn: null,
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
+
59
  // DOM μš”μ†Œ μ°Έμ‘° μ΄ˆκΈ°ν™”
60
  initElements: function() {
61
  // νƒ­ 및 μ„Ήμ…˜
62
  this.elements.deviceTab = document.getElementById('deviceTab');
63
  this.elements.deviceSection = document.getElementById('deviceSection');
64
+
65
  // μ—°κ²° κ΄€λ ¨
66
  this.elements.deviceServerUrlInput = document.getElementById('deviceServerUrlInput');
67
  this.elements.connectDeviceServerBtn = document.getElementById('connectDeviceServerBtn');
68
  this.elements.deviceConnectionStatus = document.getElementById('deviceConnectionStatus');
69
+
70
  // κΈ°λ³Έ κΈ°λŠ₯
71
  this.elements.deviceBasicFunctions = document.getElementById('deviceBasicFunctions');
72
  this.elements.checkDeviceStatusBtn = document.getElementById('checkDeviceStatusBtn');
73
  this.elements.deviceStatusResult = document.getElementById('deviceStatusResult');
74
+
75
+ // ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ (미리 μ •μ˜λœ)
76
  this.elements.deviceProgramControl = document.getElementById('deviceProgramControl');
77
  this.elements.getProgramsBtn = document.getElementById('getProgramsBtn');
78
  this.elements.programsList = document.getElementById('programsList');
79
  this.elements.programSelectDropdown = document.getElementById('programSelectDropdown');
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
  },
93
+
94
  // 이벀트 λ¦¬μŠ€λ„ˆ 등둝
95
  initEventListeners: function() {
96
  // νƒ­ μ „ν™˜
 
100
  this.switchToDeviceTab();
101
  });
102
  }
103
+
104
  // μ„œλ²„ μ—°κ²°
105
  if (this.elements.connectDeviceServerBtn) {
106
  this.elements.connectDeviceServerBtn.addEventListener('click', () => {
 
108
  this.connectServer();
109
  });
110
  }
111
+
112
  // μ—”ν„° ν‚€λ‘œ μ—°κ²°
113
  if (this.elements.deviceServerUrlInput) {
114
  this.elements.deviceServerUrlInput.addEventListener('keydown', (event) => {
 
119
  }
120
  });
121
  }
122
+
123
  // μž₯치 μƒνƒœ 확인
124
  if (this.elements.checkDeviceStatusBtn) {
125
  this.elements.checkDeviceStatusBtn.addEventListener('click', () => {
 
127
  this.checkDeviceStatus();
128
  });
129
  }
130
+
131
  // ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 쑰회
132
  if (this.elements.getProgramsBtn) {
133
  this.elements.getProgramsBtn.addEventListener('click', () => {
 
135
  this.loadProgramsList();
136
  });
137
  }
138
+
139
  // ν”„λ‘œκ·Έλž¨ 선택 λ³€κ²½
140
  if (this.elements.programSelectDropdown) {
141
  this.elements.programSelectDropdown.addEventListener('change', (event) => {
 
143
  this.updateExecuteButton();
144
  });
145
  }
146
+
147
+ // ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ (미리 μ •μ˜λœ)
148
  if (this.elements.executeProgramBtn) {
149
  this.elements.executeProgramBtn.addEventListener('click', () => {
150
  const programId = this.elements.programSelectDropdown.value;
151
+ console.log(`미리 μ •μ˜λœ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ λ²„νŠΌ 클릭, μ„ νƒλœ ID: ${programId}`);
152
  this.executeProgram(programId);
153
  });
154
  }
155
+
156
+ // ================== μΆ”κ°€ μ‹œμž‘ 3/4 ==================
157
+ // μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ λ²„νŠΌ 클릭
158
+ if (this.elements.executeCustomBtn) {
159
+ this.elements.executeCustomBtn.addEventListener('click', () => {
160
+ const command = this.elements.customCommandInput.value;
161
+ console.log(`μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ λ²„νŠΌ 클릭, λͺ…λ Ήμ–΄: ${command}`);
162
+ this.executeCustomProgram(command);
163
+ });
164
+ }
165
+
166
+ // μ—”ν„° ν‚€λ‘œ μ‚¬μš©μž μ •μ˜ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰
167
+ if (this.elements.customCommandInput) {
168
+ this.elements.customCommandInput.addEventListener('keydown', (event) => {
169
+ if (event.key === 'Enter') {
170
+ console.log('μ‚¬μš©μž μ •μ˜ λͺ…λ Ήμ–΄ μž…λ ₯ ν•„λ“œμ—μ„œ μ—”ν„° ν‚€ 감지');
171
+ event.preventDefault(); // 폼 제좜 λ°©μ§€ λ“± κΈ°λ³Έ λ™μž‘ 막기
172
+ const command = this.elements.customCommandInput.value;
173
+ this.executeCustomProgram(command);
174
+ }
175
+ });
176
+ }
177
+ // ================== μΆ”κ°€ 끝 3/4 ====================
178
+
179
  console.log('μž₯치 μ œμ–΄ 이벀트 λ¦¬μŠ€λ„ˆ 등둝 μ™„λ£Œ');
180
  },
181
+
182
  // μž₯치 μ œμ–΄ νƒ­μœΌλ‘œ μ „ν™˜
183
  switchToDeviceTab: function() {
184
  // λͺ¨λ“  νƒ­κ³Ό νƒ­ μ½˜ν…μΈ  λΉ„ν™œμ„±ν™”
185
  const tabs = document.querySelectorAll('.tab');
186
  const tabContents = document.querySelectorAll('.tab-content');
187
+
188
  tabs.forEach(tab => tab.classList.remove('active'));
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
  },
197
+
198
  // μ„œλ²„ μ—°κ²° ν•¨μˆ˜
199
  connectServer: async function() {
200
  // URL κ°€μ Έμ˜€κΈ° (μž…λ ₯된 것이 있으면 λ°±μ—…μœΌλ‘œ μ‚¬μš©)
201
  const inputUrl = this.elements.deviceServerUrlInput.value.trim();
202
+
203
  // μ—°κ²° μ‹œλ„ 쀑 UI μ—…λ°μ΄νŠΈ
204
  this.elements.connectDeviceServerBtn.disabled = true;
205
  this.updateConnectionStatus('connecting', 'ν™˜κ²½λ³€μˆ˜μ— μ €μž₯된 μ„œλ²„λ‘œ μ—°κ²° μ‹œλ„ 쀑...');
206
+
207
  try {
208
  console.log('ν™˜κ²½λ³€μˆ˜μ— μ €μž₯된 μž₯치 μ„œλ²„λ‘œ μ—°κ²° μ‹œλ„');
209
+
210
  // λ°±μ—”λ“œ API ν˜ΈμΆœν•˜μ—¬ μ„œλ²„ μƒνƒœ 확인
211
  const response = await AppUtils.fetchWithTimeout('/api/device/status', {
212
  method: 'GET'
213
  }, 10000); // 10초 νƒ€μž„μ•„μ›ƒ
214
+
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이 μ—†μ–΄ μ—°κ²° μ‹€νŒ¨');
 
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',
 
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 {
 
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',
 
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; // μ„±κ³΅ν•˜λ©΄ μ—¬κΈ°μ„œ μ’…λ£Œ
 
347
  } catch (inputUrlError) {
348
  // μž…λ ₯ URL둜 μž¬μ‹œλ„ 쀑 였λ₯˜
349
  console.error('μž…λ ₯ URL둜 μž¬μ‹œλ„ 쀑 였λ₯˜ λ°œμƒ:', inputUrlError);
350
+
351
  if (inputUrlError.message.includes('μ‹œκ°„μ΄ 초과')) {
352
  this.updateConnectionStatus('error', 'μ„œλ²„ μ—°κ²° μ‹œκ°„ 초과. μ„œλ²„κ°€ μ‹€ν–‰ 쀑인지 ν™•μΈν•΄μ£Όμ„Έμš”.');
353
  } else {
 
367
  this.elements.connectDeviceServerBtn.disabled = false;
368
  }
369
  },
370
+
371
  // μ—°κ²° μƒνƒœ μ—…λ°μ΄νŠΈ
372
  updateConnectionStatus: function(status, message) {
373
  const statusElement = this.elements.deviceConnectionStatus;
374
+ if (!statusElement) return; // μš”μ†Œ μ—†μœΌλ©΄ μ’…λ£Œ
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) {
 
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);
 
427
  this.elements.checkDeviceStatusBtn.disabled = false;
428
  }
429
  },
430
+
431
  // ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 쑰회
432
  loadProgramsList: async function() {
433
  if (!this.isConnected) {
 
435
  console.error('ν”„λ‘œκ·Έλž¨ λͺ©λ‘ 쑰회 μ‹œλ„ 쀑 였λ₯˜: μ„œλ²„ μ—°κ²° μ•ˆλ¨');
436
  return;
437
  }
438
+
439
  // 이미 λ‘œλ”© 쀑이면 쀑볡 μš”μ²­ λ°©μ§€
440
  if (this.isLoadingPrograms) {
441
  console.log('이미 ν”„λ‘œκ·Έλž¨ λͺ©λ‘ λ‘œλ”© 쀑');
442
  return;
443
  }
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 {
 
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">
 
506
  `;
507
  return;
508
  }
509
+
510
  // ν…Œμ΄λΈ” ν˜•νƒœλ‘œ ν”„λ‘œκ·Έλž¨ λͺ©λ‘ ν‘œμ‹œ
511
  let html = `
512
  <table class="program-list">
 
519
  </thead>
520
  <tbody>
521
  `;
522
+
523
  // ν”„λ‘œκ·Έλž¨ ν•­λͺ© 생성
524
  this.programsList.forEach(program => {
525
  html += `
 
530
  </tr>
531
  `;
532
  });
533
+
534
  html += `
535
  </tbody>
536
  </table>
 
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 = '';
 
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
  },
575
+
576
  // μ‹€ν–‰ λ²„νŠΌ μƒνƒœ μ—…λ°μ΄νŠΈ
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
+
600
  // μ‹€ν–‰ 쀑 UI μ—…λ°μ΄νŠΈ
601
+ if(this.elements.executeProgramBtn) this.elements.executeProgramBtn.disabled = true;
602
  this.showExecuteResult('loading', 'ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 쀑...');
603
+
604
  try {
605
  console.log(`ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­ 전솑: ${programId}`);
606
+
607
  // λ°±μ—”λ“œ API 호좜
608
  const response = await AppUtils.fetchWithTimeout(`/api/device/programs/${programId}/execute`, {
609
  method: 'POST',
 
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 {
 
630
  } catch (error) {
631
  // μ˜ˆμ™Έ λ°œμƒ
632
  console.error('ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ:', error);
633
+
634
  if (error.message.includes('μ‹œκ°„μ΄ 초과')) {
635
  this.showExecuteResult('error', 'ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μš”μ²­ μ‹œκ°„ 초과. μ„œλ²„ 응닡이 μ—†μŠ΅λ‹ˆλ‹€.');
636
  } else {
 
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
  },
652
+
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">
 
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':
 
708
  default:
709
  resultElement.textContent = message;
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
+
728
+ // μ‹€ν–‰ 쀑 UI μ—…λ°μ΄νŠΈ
729
+ if(this.elements.executeCustomBtn) this.elements.executeCustomBtn.disabled = true;
730
+ this.showCustomExecuteResult('loading', 'λͺ…λ Ήμ–΄ μ‹€ν–‰ 쀑...');
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
+ },
789
+
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':
809
+ resultElement.classList.add('error');
810
+ resultElement.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${message}`;
811
+ break;
812
+ case 'warning':
813
+ resultElement.classList.add('warning');
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() {
829
  console.log('μž₯치 μ œμ–΄ λͺ¨λ“ˆ λ‘œλ“œλ¨');
830
+
831
  // DOM이 μ™„μ „νžˆ λ‘œλ“œλœ ν›„ μ•½κ°„μ˜ 지연을 두고 μ΄ˆκΈ°ν™”
832
+ // DOM μš”μ†Œκ°€ ν™•μ‹€νžˆ λ‘œλ“œλœ 후에 μ΄ˆκΈ°ν™”ν•˜κΈ° μœ„ν•¨
833
  setTimeout(() => {
834
+ // AppUtilsκ°€ μ •μ˜λ˜μ—ˆλŠ”μ§€ 확인 (μ˜μ‘΄μ„±)
835
+ if (typeof AppUtils === 'undefined') {
836
+ console.error('AppUtilsκ°€ μ •μ˜λ˜μ§€ μ•Šμ•„ DeviceControl μ΄ˆκΈ°ν™” μ‹€νŒ¨.');
837
+ // ν•„μš”ν•˜λ‹€λ©΄ μ‚¬μš©μžμ—κ²Œ μ•Œλ¦Ό ν‘œμ‹œ
838
+ alert('νŽ˜μ΄μ§€ μ΄ˆκΈ°ν™” 였λ₯˜: ν•„μˆ˜ μœ ν‹Έλ¦¬ν‹°(AppUtils)λ₯Ό λ‘œλ“œν•  수 μ—†μŠ΅λ‹ˆλ‹€.');
839
+ return;
840
+ }
841
  DeviceControl.init();
842
  }, 100);
843
+ });
app/templates/index.html CHANGED
@@ -121,6 +121,7 @@
121
  </div>
122
  </div>
123
  </section>
 
124
  <!-- μž₯치 μ œμ–΄ νƒ­ -->
125
  <section id="deviceSection" class="tab-content">
126
  <div class="device-connection">
@@ -156,6 +157,16 @@
156
  <button id="executeProgramBtn" class="execute-btn" disabled>μ„ νƒν•œ ν”„λ‘œκ·Έλž¨ μ‹€ν–‰</button>
157
  <div id="executeResult" class="execute-result"></div>
158
  </div>
 
 
 
 
 
 
 
 
 
 
159
  </section>
160
  </main>
161
 
 
121
  </div>
122
  </div>
123
  </section>
124
+
125
  <!-- μž₯치 μ œμ–΄ νƒ­ -->
126
  <section id="deviceSection" class="tab-content">
127
  <div class="device-connection">
 
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">
165
+ <input type="text" id="customCommandInput" placeholder="μ‹€ν–‰ν•  λͺ…λ Ήμ–΄ λ˜λŠ” ν”„λ‘œκ·Έλž¨ 경둜 μž…λ ₯">
166
+ <button id="executeCustomBtn">μ‹€ν–‰</button>
167
+ </div>
168
+ <div id="customExecuteResult" class="execute-result"></div>
169
+ </div>
170
  </section>
171
  </main>
172