jeongsoo commited on
Commit
867f974
ยท
1 Parent(s): 12b4400
app/static/css/device-style.css CHANGED
@@ -1,306 +1,316 @@
1
- /**
2
- * ์žฅ์น˜ ๊ด€๋ฆฌ ์ „์šฉ CSS ์Šคํƒ€์ผ
3
- */
4
 
5
- /* ์žฅ์น˜ ๊ด€๋ฆฌ ์„น์…˜ */
6
- .device-container {
7
- background-color: var(--card-bg);
8
- border-radius: 8px;
9
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
10
- overflow: hidden;
11
- padding: 20px;
12
- }
13
-
14
- /* ์„œ๋ฒ„ ์ƒํƒœ ํ‘œ์‹œ */
15
- .server-status {
16
- padding: 12px 15px;
17
- border-radius: 6px;
18
- margin-bottom: 15px;
19
  display: flex;
20
- align-items: center;
21
- }
22
-
23
- .server-status i {
24
- margin-right: 10px;
25
- font-size: 18px;
26
- }
27
-
28
- .server-status.success {
29
- background-color: rgba(16, 185, 129, 0.1);
30
- color: var(--success-color);
31
- border-left: 4px solid var(--success-color);
32
- }
33
-
34
- .server-status.error {
35
- background-color: rgba(239, 68, 68, 0.1);
36
- color: var(--error-color);
37
- border-left: 4px solid var(--error-color);
38
- }
39
-
40
- .server-status.warning {
41
- background-color: rgba(245, 158, 11, 0.1);
42
- color: var(--secondary-color);
43
- border-left: 4px solid var(--secondary-color);
44
  }
45
 
46
- /* ์„œ๋ฒ„ ์‹œ์ž‘ ์•ˆ๋‚ด */
47
- .server-guide {
48
- background-color: #f8f9fa;
49
- border-radius: 6px;
50
  padding: 15px;
51
- margin: 15px 0;
52
- font-size: 14px;
53
  }
54
 
55
- .server-guide code {
56
- background-color: #e9ecef;
57
- padding: 2px 6px;
58
- border-radius: 4px;
59
- font-family: monospace;
60
  }
61
 
62
- .server-guide ol {
63
- margin-left: 20px;
64
- margin-top: 10px;
65
- margin-bottom: 0;
66
  }
67
 
68
- .server-guide li {
69
- margin-bottom: 5px;
 
 
 
 
70
  }
71
 
72
- /* ์žฌ์‹œ๋„ ๋ฒ„ํŠผ */
73
- .retry-button {
74
  background-color: var(--primary-color);
75
  color: white;
76
  border: none;
77
  border-radius: 4px;
78
- padding: 8px 15px;
79
- margin-top: 10px;
80
  cursor: pointer;
81
- display: flex;
82
- align-items: center;
83
- justify-content: center;
84
- font-size: 14px;
85
- transition: var(--transition);
86
  }
87
 
88
- .retry-button:hover {
89
- background-color: var(--primary-dark);
90
  }
91
 
92
- .retry-button i {
93
- margin-right: 5px;
 
94
  }
95
 
96
- /* ์žฅ์น˜ ๋ชฉ๋ก */
97
- .device-section, .programs-section {
98
- margin-top: 25px;
 
99
  }
100
 
101
- .device-section h2, .programs-section h2 {
102
- margin-bottom: 15px;
103
- color: var(--primary-color);
104
- display: flex;
105
- justify-content: space-between;
106
- align-items: center;
 
 
 
 
107
  }
108
 
109
- .device-count {
110
- font-size: 14px;
111
- color: var(--light-text);
112
- margin-bottom: 10px;
113
  }
114
 
115
- .device-item {
116
- background-color: #f8f9fa;
 
117
  border-radius: 8px;
118
  padding: 15px;
119
- margin-bottom: 10px;
120
- border: 1px solid var(--border-color);
121
- transition: var(--transition);
122
  }
123
 
124
- .device-item:hover {
125
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
126
  }
127
 
128
- .device-item h3 {
129
- margin-bottom: 10px;
130
- color: var(--primary-color);
131
- font-size: 16px;
 
132
  }
133
 
134
- .device-details {
135
- display: grid;
136
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
137
  gap: 10px;
138
- font-size: 14px;
 
139
  }
140
 
141
- .device-details p {
142
- margin: 0;
 
 
 
 
 
 
 
143
  }
144
 
145
- .status-connected, .status-online {
146
- color: var(--success-color);
147
- font-weight: 500;
148
  }
149
 
150
- .status-disconnected, .status-offline {
151
- color: var(--error-color);
152
- font-weight: 500;
153
  }
154
 
155
- .status-idle {
156
- color: var(--secondary-color);
157
- font-weight: 500;
 
 
 
 
 
 
 
 
 
 
158
  }
159
 
160
- .no-devices, .no-programs {
161
- padding: 15px;
162
- text-align: center;
163
- background-color: #f8f9fa;
164
  border-radius: 8px;
165
- color: var(--light-text);
166
- border: 1px dashed var(--border-color);
 
167
  }
168
 
169
- .no-devices i, .no-programs i {
170
- margin-right: 5px;
171
  }
172
 
173
- /* ๋กœ๋”ฉ ํ‘œ์‹œ */
174
- .loading-device, .loading-device-list, .loading-programs {
175
- display: flex;
176
- flex-direction: column;
177
- align-items: center;
178
- justify-content: center;
179
- padding: 20px;
180
- text-align: center;
181
  }
182
 
183
- .spinner.small {
184
- width: 20px;
185
- height: 20px;
186
- margin-right: 8px;
187
  }
188
 
189
- /* ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์„น์…˜ */
190
- .programs-container {
191
- background-color: #f8f9fa;
192
- border-radius: 8px;
193
- padding: 15px;
194
- margin-top: 20px;
195
- border: 1px solid var(--border-color);
196
  }
197
 
198
- .program-item {
199
- background-color: white;
200
- border-radius: 6px;
201
- padding: 15px;
202
- margin-bottom: 10px;
203
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
204
- position: relative;
 
 
205
  }
206
 
207
- .program-item h3 {
208
- margin-bottom: 8px;
209
- color: var(--primary-color);
210
- font-size: 16px;
211
  }
212
 
213
- .program-description {
214
- font-size: 14px;
215
- color: var(--light-text);
216
  margin-bottom: 15px;
217
  }
218
 
 
 
 
 
 
 
 
 
 
219
  .execute-btn {
220
  background-color: var(--primary-color);
221
  color: white;
222
  border: none;
223
  border-radius: 4px;
224
- padding: 8px 15px;
225
  cursor: pointer;
226
- display: flex;
227
- align-items: center;
228
- font-size: 14px;
229
- transition: var(--transition);
230
  }
231
 
232
  .execute-btn:hover {
233
- background-color: var(--primary-dark);
234
  }
235
 
236
- .execute-btn i {
237
- margin-right: 5px;
 
238
  }
239
 
240
- .execute-loading, .execute-success, .execute-error {
241
- display: flex;
242
- align-items: center;
243
- padding: 5px 10px;
244
  border-radius: 4px;
245
  margin-top: 10px;
246
- font-size: 14px;
247
  }
248
 
249
- .execute-loading {
250
- background-color: #f1f5f9;
251
- color: var(--light-text);
 
252
  }
253
 
254
- .execute-success {
255
- background-color: rgba(16, 185, 129, 0.1);
256
- color: var(--success-color);
 
257
  }
258
 
259
- .execute-error {
260
- background-color: rgba(239, 68, 68, 0.1);
261
- color: var(--error-color);
 
262
  }
263
 
264
- .execute-loading span, .execute-success span, .execute-error span {
265
- margin-left: 5px;
 
 
 
 
 
 
 
 
 
266
  }
267
 
268
- /* ์žฅ์น˜ ๊ด€๋ฆฌ ์„น์…˜ ์ƒ๋‹จ ๋ฒ„ํŠผ */
269
- .device-toolbar {
270
- display: flex;
271
- justify-content: space-between;
272
- margin-bottom: 15px;
273
  }
274
 
275
- .load-programs-btn, .refresh-device-btn {
276
- padding: 8px 15px;
277
- background-color: var(--primary-color);
278
- color: white;
279
- border: none;
280
- border-radius: 4px;
281
- cursor: pointer;
282
  display: flex;
283
  align-items: center;
284
- font-size: 14px;
285
- transition: var(--transition);
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  }
287
 
288
- .load-programs-btn:hover, .refresh-device-btn:hover {
289
- background-color: var(--primary-dark);
 
 
 
 
 
 
 
290
  }
291
 
292
- .load-programs-btn i, .refresh-device-btn i {
293
- margin-right: 5px;
 
 
 
 
 
 
 
 
 
294
  }
295
 
296
- /* ๋ฐ˜์‘ํ˜• */
297
- @media (max-width: 768px) {
298
- .device-details {
299
- grid-template-columns: 1fr;
300
- }
301
-
302
- .device-toolbar {
303
- flex-direction: column;
304
- gap: 10px;
305
- }
306
  }
 
1
+ /* ์žฅ์น˜ ์ œ์–ด ๊ด€๋ จ ์Šคํƒ€์ผ */
 
 
2
 
3
+ /* ์žฅ์น˜ ์ œ์–ด ์„น์…˜ */
4
+ #deviceSection {
 
 
 
 
 
 
 
 
 
 
 
 
5
  display: flex;
6
+ flex-direction: column;
7
+ gap: 20px;
8
+ max-width: 1000px;
9
+ margin: 0 auto;
10
+ padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
12
 
13
+ /* ์žฅ์น˜ ์—ฐ๊ฒฐ ์ปจํ…Œ์ด๋„ˆ */
14
+ .device-connection {
15
+ background-color: var(--bg-color-secondary);
16
+ border-radius: 8px;
17
  padding: 15px;
18
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 
19
  }
20
 
21
+ .device-connection h3 {
22
+ margin-top: 0;
23
+ margin-bottom: 15px;
24
+ color: var(--text-color-primary);
25
+ font-size: 1.2rem;
26
  }
27
 
28
+ .device-connection-form {
29
+ display: flex;
30
+ gap: 10px;
31
+ margin-bottom: 15px;
32
  }
33
 
34
+ .device-connection-form input {
35
+ flex: 1;
36
+ padding: 10px;
37
+ border: 1px solid var(--border-color);
38
+ border-radius: 4px;
39
+ font-size: 0.95rem;
40
  }
41
 
42
+ .device-connection-form button {
 
43
  background-color: var(--primary-color);
44
  color: white;
45
  border: none;
46
  border-radius: 4px;
47
+ padding: 10px 15px;
 
48
  cursor: pointer;
49
+ font-weight: 500;
50
+ transition: background-color 0.2s;
 
 
 
51
  }
52
 
53
+ .device-connection-form button:hover {
54
+ background-color: var(--primary-color-dark);
55
  }
56
 
57
+ .device-connection-form button:disabled {
58
+ background-color: var(--disabled-color);
59
+ cursor: not-allowed;
60
  }
61
 
62
+ .connection-status {
63
+ padding: 10px;
64
+ border-radius: 4px;
65
+ font-size: 0.9rem;
66
  }
67
 
68
+ .connection-status.connected {
69
+ background-color: rgba(25, 135, 84, 0.1);
70
+ color: #198754;
71
+ border: 1px solid rgba(25, 135, 84, 0.2);
72
+ }
73
+
74
+ .connection-status.disconnected {
75
+ background-color: rgba(108, 117, 125, 0.1);
76
+ color: #6c757d;
77
+ border: 1px solid rgba(108, 117, 125, 0.2);
78
  }
79
 
80
+ .connection-status.error {
81
+ background-color: rgba(220, 53, 69, 0.1);
82
+ color: #dc3545;
83
+ border: 1px solid rgba(220, 53, 69, 0.2);
84
  }
85
 
86
+ /* ์žฅ์น˜ ๊ธฐ๋Šฅ ์ปจํ…Œ์ด๋„ˆ */
87
+ .device-functions {
88
+ background-color: var(--bg-color-secondary);
89
  border-radius: 8px;
90
  padding: 15px;
91
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
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);
103
+ font-size: 1.2rem;
104
  }
105
 
106
+ .function-buttons {
107
+ display: flex;
 
108
  gap: 10px;
109
+ margin-bottom: 15px;
110
+ flex-wrap: wrap;
111
  }
112
 
113
+ .function-buttons button {
114
+ background-color: var(--secondary-color);
115
+ color: white;
116
+ border: none;
117
+ border-radius: 4px;
118
+ padding: 8px 12px;
119
+ cursor: pointer;
120
+ font-size: 0.9rem;
121
+ transition: background-color 0.2s;
122
  }
123
 
124
+ .function-buttons button:hover {
125
+ background-color: var(--secondary-color-dark);
 
126
  }
127
 
128
+ .function-buttons button:disabled {
129
+ background-color: var(--disabled-color);
130
+ cursor: not-allowed;
131
  }
132
 
133
+ .device-status-result {
134
+ width: 100%;
135
+ min-height: 100px;
136
+ max-height: 200px;
137
+ padding: 10px;
138
+ border: 1px solid var(--border-color);
139
+ border-radius: 4px;
140
+ font-family: monospace;
141
+ font-size: 0.9rem;
142
+ overflow-y: auto;
143
+ background-color: var(--bg-color-tertiary);
144
+ margin-bottom: 15px;
145
+ resize: vertical;
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 {
173
+ width: 100%;
174
+ border-collapse: collapse;
175
+ margin-bottom: 15px;
 
 
 
176
  }
177
 
178
+ .program-list th, .program-list td {
179
+ padding: 10px;
180
+ text-align: left;
181
+ border-bottom: 1px solid var(--border-color);
182
+ }
183
+
184
+ .program-list th {
185
+ background-color: var(--bg-color-tertiary);
186
+ font-weight: 500;
187
  }
188
 
189
+ .program-list tr:hover {
190
+ background-color: var(--bg-color-tertiary);
 
 
191
  }
192
 
193
+ .program-select-container {
 
 
194
  margin-bottom: 15px;
195
  }
196
 
197
+ .program-select-container select {
198
+ width: 100%;
199
+ padding: 10px;
200
+ border: 1px solid var(--border-color);
201
+ border-radius: 4px;
202
+ font-size: 0.95rem;
203
+ background-color: var(--bg-color-tertiary);
204
+ }
205
+
206
  .execute-btn {
207
  background-color: var(--primary-color);
208
  color: white;
209
  border: none;
210
  border-radius: 4px;
211
+ padding: 10px 15px;
212
  cursor: pointer;
213
+ font-weight: 500;
214
+ transition: background-color 0.2s;
215
+ width: 100%;
216
+ margin-bottom: 15px;
217
  }
218
 
219
  .execute-btn:hover {
220
+ background-color: var(--primary-color-dark);
221
  }
222
 
223
+ .execute-btn:disabled {
224
+ background-color: var(--disabled-color);
225
+ cursor: not-allowed;
226
  }
227
 
228
+ .execute-result {
229
+ padding: 10px;
 
 
230
  border-radius: 4px;
231
  margin-top: 10px;
232
+ font-size: 0.9rem;
233
  }
234
 
235
+ .execute-result.success {
236
+ background-color: rgba(25, 135, 84, 0.1);
237
+ color: #198754;
238
+ border: 1px solid rgba(25, 135, 84, 0.2);
239
  }
240
 
241
+ .execute-result.error {
242
+ background-color: rgba(220, 53, 69, 0.1);
243
+ color: #dc3545;
244
+ border: 1px solid rgba(220, 53, 69, 0.2);
245
  }
246
 
247
+ .execute-result.warning {
248
+ background-color: rgba(255, 193, 7, 0.1);
249
+ color: #ffc107;
250
+ border: 1px solid rgba(255, 193, 7, 0.2);
251
  }
252
 
253
+ /* ๋กœ๋”ฉ ํ‘œ์‹œ */
254
+ .loading-spinner {
255
+ display: inline-block;
256
+ width: 16px;
257
+ height: 16px;
258
+ border: 2px solid rgba(0, 0, 0, 0.1);
259
+ border-radius: 50%;
260
+ border-top-color: var(--primary-color);
261
+ animation: spin 1s ease-in-out infinite;
262
+ margin-right: 8px;
263
+ vertical-align: middle;
264
  }
265
 
266
+ @keyframes spin {
267
+ to { transform: rotate(360deg); }
 
 
 
268
  }
269
 
270
+ .loading-message {
 
 
 
 
 
 
271
  display: flex;
272
  align-items: center;
273
+ justify-content: center;
274
+ padding: 20px;
275
+ font-size: 0.95rem;
276
+ color: var(--text-color-secondary);
277
+ }
278
+
279
+ /* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */
280
+ .error-message {
281
+ background-color: rgba(220, 53, 69, 0.1);
282
+ color: #dc3545;
283
+ border: 1px solid rgba(220, 53, 69, 0.2);
284
+ padding: 10px;
285
+ border-radius: 4px;
286
+ margin-top: 10px;
287
+ font-size: 0.9rem;
288
  }
289
 
290
+ /* ์—†์Œ ๋ฉ”์‹œ์ง€ */
291
+ .no-programs-message {
292
+ text-align: center;
293
+ padding: 20px;
294
+ font-size: 0.95rem;
295
+ color: var(--text-color-secondary);
296
+ border: 1px dashed var(--border-color);
297
+ border-radius: 4px;
298
+ margin-top: 10px;
299
  }
300
 
301
+ /* ์žฌ์‹œ๋„ ๋ฒ„ํŠผ */
302
+ .retry-button {
303
+ background-color: var(--secondary-color);
304
+ color: white;
305
+ border: none;
306
+ border-radius: 4px;
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
  }
app/static/js/app-core.js CHANGED
@@ -1,326 +1,112 @@
1
  /**
2
- * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI ์ฝ”์–ด JavaScript
3
  */
4
 
5
- // ์ „์—ญ ๋ณ€์ˆ˜
6
- let currentLLM = 'openai';
7
- let supportedLLMs = [];
8
-
9
- // DOM ๋ณ€์ˆ˜ ๋ฏธ๋ฆฌ ์„ ์–ธ
10
- let chatTab, docsTab, deviceTab, chatSection, docsSection, deviceSection;
11
- let chatMessages, userInput, sendButton;
12
- let micButton, stopRecordingButton, recordingStatus;
13
- let llmSelect, currentLLMInfo;
14
-
15
- // ๋…น์Œ ๊ด€๋ จ ๋ณ€์ˆ˜
16
- let mediaRecorder = null;
17
- let audioChunks = [];
18
- let isRecording = false;
19
-
20
- /**
21
- * ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ํ•จ์ˆ˜
22
- */
23
- async function checkAppStatus() {
24
- try {
25
- const response = await fetch('/api/status');
26
- if (!response.ok) {
27
- return false;
 
28
  }
29
- const data = await response.json();
30
- return data.ready;
31
- } catch (error) {
32
- console.error('์ƒํƒœ ํ™•์ธ ์‹คํŒจ:', error);
33
- return false;
34
- }
35
- }
36
-
37
- /**
38
- * DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
39
- */
40
- function initDomElements() {
41
- console.log('DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์ค‘...');
42
-
43
- // ํƒญ ๊ด€๋ จ ์š”์†Œ
44
- chatTab = document.getElementById('chatTab');
45
- docsTab = document.getElementById('docsTab');
46
- deviceTab = document.getElementById('deviceTab');
47
- chatSection = document.getElementById('chatSection');
48
- docsSection = document.getElementById('docsSection');
49
- deviceSection = document.getElementById('deviceSection');
50
-
51
- // ์ฑ„ํŒ… ๊ด€๋ จ ์š”์†Œ
52
- chatMessages = document.getElementById('chatMessages');
53
- userInput = document.getElementById('userInput');
54
- sendButton = document.getElementById('sendButton');
55
-
56
- // ์Œ์„ฑ ๋…น์Œ ๊ด€๋ จ ์š”์†Œ
57
- micButton = document.getElementById('micButton');
58
- stopRecordingButton = document.getElementById('stopRecordingButton');
59
- recordingStatus = document.getElementById('recordingStatus');
60
-
61
- // LLM ๊ด€๋ จ ์š”์†Œ
62
- llmSelect = document.getElementById('llmSelect');
63
- currentLLMInfo = document.getElementById('currentLLMInfo');
64
-
65
- console.log('DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
66
- }
67
-
68
- /**
69
- * ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
70
- */
71
- function initEventListeners() {
72
- console.log('์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์ค‘...');
73
-
74
- // ํƒญ ์ „ํ™˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
75
- chatTab.addEventListener('click', () => {
76
- switchTab('chat');
77
- });
78
 
79
- docsTab.addEventListener('click', () => {
80
- switchTab('docs');
81
- loadDocuments();
82
- });
83
-
84
- deviceTab.addEventListener('click', () => {
85
- switchTab('device');
86
- loadDeviceStatus();
87
- });
88
-
89
- // LLM ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
90
- llmSelect.addEventListener('change', (event) => {
91
- changeLLM(event.target.value);
92
- });
93
-
94
- // ๋ฉ”์‹œ์ง€ ์ „์†ก ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
95
- sendButton.addEventListener('click', sendMessage);
96
- userInput.addEventListener('keydown', (event) => {
97
- if (event.key === 'Enter' && !event.shiftKey) {
98
- event.preventDefault();
99
- sendMessage();
 
 
 
100
  }
101
- });
102
-
103
- // ์Œ์„ฑ ์ธ์‹ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
104
- micButton.addEventListener('click', startRecording);
105
- stopRecordingButton.addEventListener('click', stopRecording);
106
-
107
- // ์ž๋™ ์ž…๋ ฅ ํ•„๋“œ ํฌ๊ธฐ ์กฐ์ •
108
- userInput.addEventListener('input', adjustTextareaHeight);
109
-
110
- console.log('์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
111
- }
112
-
113
- /**
114
- * ํƒญ ์ „ํ™˜ ํ•จ์ˆ˜
115
- * @param {string} tabName - ํ™œ์„ฑํ™”ํ•  ํƒญ ์ด๋ฆ„ ('chat', 'docs', ๋˜๋Š” 'device')
116
- */
117
- function switchTab(tabName) {
118
- console.log(`ํƒญ ์ „ํ™˜: ${tabName}`);
119
-
120
- // ๋ชจ๋“  ํƒญ์„ ๋น„ํ™œ์„ฑํ™”
121
- [chatTab, docsTab, deviceTab].forEach(tab => tab.classList.remove('active'));
122
- [chatSection, docsSection, deviceSection].forEach(section => section.classList.remove('active'));
123
-
124
- // ์„ ํƒํ•œ ํƒญ ํ™œ์„ฑํ™”
125
- if (tabName === 'chat') {
126
- chatTab.classList.add('active');
127
- chatSection.classList.add('active');
128
- } else if (tabName === 'docs') {
129
- docsTab.classList.add('active');
130
- docsSection.classList.add('active');
131
- } else if (tabName === 'device') {
132
- deviceTab.classList.add('active');
133
- deviceSection.classList.add('active');
134
- }
135
- }
136
-
137
- /**
138
- * ์‹œ์Šคํ…œ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
139
- * @param {string} message - ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€
140
- */
141
- function addSystemNotification(message) {
142
- console.log(`์‹œ์Šคํ…œ ์•Œ๋ฆผ ์ถ”๊ฐ€: ${message}`);
143
-
144
- const messageDiv = document.createElement('div');
145
- messageDiv.classList.add('message', 'system');
146
-
147
- const contentDiv = document.createElement('div');
148
- contentDiv.classList.add('message-content');
149
 
150
- const messageP = document.createElement('p');
151
- messageP.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
152
- contentDiv.appendChild(messageP);
153
-
154
- messageDiv.appendChild(contentDiv);
155
- chatMessages.appendChild(messageDiv);
156
-
157
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
158
- chatMessages.scrollTop = chatMessages.scrollHeight;
159
- }
160
-
161
- /**
162
- * ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
163
- * @param {string} text - ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
164
- * @param {string} sender - ๋ฉ”์‹œ์ง€ ๋ฐœ์‹ ์ž ('user' ๋˜๋Š” 'bot' ๋˜๋Š” 'system')
165
- * @param {string|null} transcription - ์Œ์„ฑ ์ธ์‹ ํ…์ŠคํŠธ (์„ ํƒ ์‚ฌํ•ญ)
166
- * @param {Array|null} sources - ์†Œ์Šค ์ •๋ณด ๋ฐฐ์—ด (์„ ํƒ ์‚ฌํ•ญ)
167
- */
168
- function addMessage(text, sender, transcription = null, sources = null) {
169
- console.log(`๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€: ${sender}`);
170
-
171
- const messageDiv = document.createElement('div');
172
- messageDiv.classList.add('message', sender);
173
-
174
- const contentDiv = document.createElement('div');
175
- contentDiv.classList.add('message-content');
176
-
177
- // ์Œ์„ฑ ์ธ์‹ ํ…์ŠคํŠธ ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
178
- if (transcription && sender === 'bot') {
179
- const transcriptionP = document.createElement('p');
180
- transcriptionP.classList.add('transcription');
181
- transcriptionP.textContent = `"${transcription}"`;
182
- contentDiv.appendChild(transcriptionP);
183
- }
184
-
185
- // ๋ฉ”์‹œ์ง€ ํ…์ŠคํŠธ ์ถ”๊ฐ€
186
- const textP = document.createElement('p');
187
- textP.textContent = text;
188
- contentDiv.appendChild(textP);
189
-
190
- // ์†Œ์Šค ์ •๋ณด ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
191
- if (sources && sources.length > 0 && sender === 'bot') {
192
- const sourcesDiv = document.createElement('div');
193
- sourcesDiv.classList.add('sources');
194
 
195
- const sourcesTitle = document.createElement('strong');
196
- sourcesTitle.textContent = '์ถœ์ฒ˜: ';
197
- sourcesDiv.appendChild(sourcesTitle);
198
 
199
- sources.forEach((source, index) => {
200
- if (index < 3) { // ์ตœ๋Œ€ 3๊ฐœ๊นŒ์ง€๋งŒ ํ‘œ์‹œ
201
- const sourceSpan = document.createElement('span');
202
- sourceSpan.classList.add('source-item');
203
- sourceSpan.textContent = source.source;
204
- sourcesDiv.appendChild(sourceSpan);
 
 
 
 
 
 
 
205
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  });
207
-
208
- contentDiv.appendChild(sourcesDiv);
209
- }
210
-
211
- messageDiv.appendChild(contentDiv);
212
- chatMessages.appendChild(messageDiv);
213
-
214
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
215
- chatMessages.scrollTop = chatMessages.scrollHeight;
216
- }
217
-
218
- /**
219
- * ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
220
- * @returns {string} ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
221
- */
222
- function addLoadingMessage() {
223
- console.log('๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€');
224
-
225
- const id = 'loading-' + Date.now();
226
- const messageDiv = document.createElement('div');
227
- messageDiv.classList.add('message', 'bot');
228
- messageDiv.id = id;
229
-
230
- const contentDiv = document.createElement('div');
231
- contentDiv.classList.add('message-content');
232
-
233
- const loadingP = document.createElement('p');
234
- loadingP.innerHTML = '<div class="spinner" style="width: 20px; height: 20px; display: inline-block; margin-right: 10px;"></div> ์ƒ๊ฐ ์ค‘...';
235
- contentDiv.appendChild(loadingP);
236
-
237
- messageDiv.appendChild(contentDiv);
238
- chatMessages.appendChild(messageDiv);
239
 
240
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
241
- chatMessages.scrollTop = chatMessages.scrollHeight;
242
-
243
- return id;
244
- }
245
-
246
- /**
247
- * ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ ํ•จ์ˆ˜
248
- * @param {string} id - ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
249
- */
250
- function removeLoadingMessage(id) {
251
- console.log(`๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ: ${id}`);
252
-
253
- const loadingMessage = document.getElementById(id);
254
- if (loadingMessage) {
255
- loadingMessage.remove();
256
  }
257
- }
258
-
259
- /**
260
- * ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
261
- * @param {string} errorText - ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
262
- */
263
- function addErrorMessage(errorText) {
264
- console.log(`์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€: ${errorText}`);
265
-
266
- const messageDiv = document.createElement('div');
267
- messageDiv.classList.add('message', 'system');
268
-
269
- const contentDiv = document.createElement('div');
270
- contentDiv.classList.add('message-content');
271
- contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
272
- contentDiv.style.color = 'var(--error-color)';
273
-
274
- const errorP = document.createElement('p');
275
- errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`;
276
- contentDiv.appendChild(errorP);
277
-
278
- messageDiv.appendChild(contentDiv);
279
- chatMessages.appendChild(messageDiv);
280
-
281
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
282
- chatMessages.scrollTop = chatMessages.scrollHeight;
283
- }
284
-
285
- /**
286
- * textarea ๋†’์ด ์ž๋™ ์กฐ์ • ํ•จ์ˆ˜
287
- */
288
- function adjustTextareaHeight() {
289
- userInput.style.height = 'auto';
290
- userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
291
- }
292
 
293
- // ์•ฑ ์ดˆ๊ธฐํ™” (ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ)
294
  document.addEventListener('DOMContentLoaded', function() {
295
- console.log('ํŽ˜์ด์ง€ ๋กœ๋“œ ์™„๋ฃŒ, ์•ฑ ์ดˆ๊ธฐํ™” ์‹œ์ž‘');
296
-
297
- // DOM ์š”์†Œ ์ดˆ๊ธฐํ™”
298
- initDomElements();
299
-
300
- // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
301
- initEventListeners();
302
-
303
- // ์•ฑ ์ƒํƒœ ํ™•์ธ (๋กœ๋”ฉ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ)
304
- if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) {
305
- // ์•ฑ ์ƒํƒœ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
306
- const statusInterval = setInterval(async () => {
307
- const isReady = await checkAppStatus();
308
- if (isReady) {
309
- clearInterval(statusInterval);
310
- console.log('์•ฑ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
311
-
312
- // ์•ฑ์ด ์ค€๋น„๋˜๋ฉด LLM ๋ชฉ๋ก ๋กœ๋“œ
313
- loadLLMs();
314
-
315
- // ํ™œ์„ฑ ํƒญ ํ™•์ธ ๋ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ
316
- if (docsSection.classList.contains('active')) {
317
- loadDocuments();
318
- } else if (deviceSection.classList.contains('active')) {
319
- loadDeviceStatus();
320
- }
321
- }
322
- }, 5000);
323
- }
324
-
325
- console.log('์•ฑ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
326
  });
 
1
  /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ JavaScript
3
  */
4
 
5
+ // ์ „์—ญ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
6
+ const AppUtils = {
7
+ // ์‹œ์Šคํ…œ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
8
+ addSystemNotification: function(message) {
9
+ console.log(`[์‹œ์Šคํ…œ ์•Œ๋ฆผ] ${message}`);
10
+
11
+ const messageDiv = document.createElement('div');
12
+ messageDiv.classList.add('message', 'system');
13
+
14
+ const contentDiv = document.createElement('div');
15
+ contentDiv.classList.add('message-content');
16
+
17
+ const messageP = document.createElement('p');
18
+ messageP.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
19
+ contentDiv.appendChild(messageP);
20
+
21
+ messageDiv.appendChild(contentDiv);
22
+
23
+ // ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์˜์—ญ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
24
+ const chatMessages = document.getElementById('chatMessages');
25
+ if (chatMessages) {
26
+ chatMessages.appendChild(messageDiv);
27
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
28
+ chatMessages.scrollTop = chatMessages.scrollHeight;
29
  }
30
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
33
+ addErrorMessage: function(errorText) {
34
+ console.error(`[์˜ค๋ฅ˜] ${errorText}`);
35
+
36
+ const messageDiv = document.createElement('div');
37
+ messageDiv.classList.add('message', 'system');
38
+
39
+ const contentDiv = document.createElement('div');
40
+ contentDiv.classList.add('message-content');
41
+ contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
42
+ contentDiv.style.color = 'var(--error-color)';
43
+
44
+ const errorP = document.createElement('p');
45
+ errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`;
46
+ contentDiv.appendChild(errorP);
47
+
48
+ messageDiv.appendChild(contentDiv);
49
+
50
+ // ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์˜์—ญ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
51
+ const chatMessages = document.getElementById('chatMessages');
52
+ if (chatMessages) {
53
+ chatMessages.appendChild(messageDiv);
54
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
55
+ chatMessages.scrollTop = chatMessages.scrollHeight;
56
  }
57
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ // ํƒ€์ž„์•„์›ƒ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” fetch
60
+ fetchWithTimeout: async function(url, options = {}, timeout = 5000) {
61
+ console.log(`API ์š”์ฒญ: ${options.method || 'GET'} ${url}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ const controller = new AbortController();
64
+ const id = setTimeout(() => controller.abort(), timeout);
 
65
 
66
+ try {
67
+ const response = await fetch(url, {
68
+ ...options,
69
+ signal: controller.signal
70
+ });
71
+ clearTimeout(id);
72
+ console.log(`API ์‘๋‹ต ์ƒํƒœ: ${response.status}`);
73
+ return response;
74
+ } catch (error) {
75
+ clearTimeout(id);
76
+ if (error.name === 'AbortError') {
77
+ console.error(`API ์š”์ฒญ ํƒ€์ž„์•„์›ƒ: ${url}`);
78
+ throw new Error('์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
79
  }
80
+ console.error(`API ์š”์ฒญ ์‹คํŒจ: ${url}`, error);
81
+ throw error;
82
+ }
83
+ },
84
+
85
+ // ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ HTML ์ƒ์„ฑ
86
+ createLoadingSpinner: function() {
87
+ return '<div class="loading-spinner"></div>';
88
+ },
89
+
90
+ // ๋‚ ์งœ ํฌ๋งทํŒ…
91
+ formatDate: function(date) {
92
+ return new Date(date).toLocaleString('ko-KR', {
93
+ year: 'numeric',
94
+ month: '2-digit',
95
+ day: '2-digit',
96
+ hour: '2-digit',
97
+ minute: '2-digit'
98
  });
99
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ // HTML ๋ฌธ์ž์—ด ์ด์Šค์ผ€์ดํ”„ (XSS ๋ฐฉ์ง€)
102
+ escapeHtml: function(html) {
103
+ const div = document.createElement('div');
104
+ div.textContent = html;
105
+ return div.innerHTML;
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์™„๋ฃŒ ์‹œ ๊ณตํ†ต ์ดˆ๊ธฐํ™”
110
  document.addEventListener('DOMContentLoaded', function() {
111
+ console.log('์•ฑ ์ฝ”์–ด ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  });
app/static/js/app-device.js CHANGED
@@ -1,375 +1,556 @@
1
  /**
2
- * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์žฅ์น˜ ๊ด€๋ฆฌ JavaScript
3
  */
4
 
5
- // ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ๊ธฐ๋ณธ URL (ํฌํŠธ ๋ณ€๊ฒฝ: 5050)
6
- const DEVICE_SERVER_URL = 'http://localhost:5050';
7
-
8
- // DOM ์š”์†Œ ๋ฏธ๋ฆฌ ์„ ์–ธ
9
- let deviceStatus, deviceList, programsList, deviceRefreshButton;
10
- let programsContainer, loadProgramsButton;
11
-
12
- /**
13
- * ์žฅ์น˜ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™”
14
- */
15
- function initDeviceElements() {
16
- console.log('์žฅ์น˜ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์ค‘...');
17
 
18
- deviceStatus = document.getElementById('deviceStatus');
19
- deviceList = document.getElementById('deviceList');
20
- programsList = document.getElementById('programsList');
21
- deviceRefreshButton = document.getElementById('deviceRefreshButton');
22
- programsContainer = document.getElementById('programsContainer');
23
- loadProgramsButton = document.getElementById('loadProgramsButton');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- console.log('์žฅ์น˜ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
26
-
27
- // ์žฅ์น˜ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
28
- initDeviceEventListeners();
29
- }
30
-
31
- /**
32
- * ์žฅ์น˜ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
33
- */
34
- function initDeviceEventListeners() {
35
- console.log('์žฅ์น˜ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์ค‘...');
36
-
37
- // ์žฅ์น˜ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ
38
- deviceRefreshButton.addEventListener('click', () => {
39
- console.log('์žฅ์น˜ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์š”์ฒญ');
40
- loadDeviceStatus();
41
- });
42
-
43
- // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ๋ฒ„ํŠผ
44
- loadProgramsButton.addEventListener('click', () => {
45
- console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์š”์ฒญ');
46
- loadProgramsList();
47
- });
48
-
49
- console.log('์žฅ์น˜ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
50
- }
51
-
52
- /**
53
- * ํƒ€์ž„์•„์›ƒ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” fetch
54
- * @param {string} url - ์š”์ฒญ URL
55
- * @param {Object} options - fetch ์˜ต์…˜
56
- * @param {number} timeout - ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„(ms)
57
- * @returns {Promise} - fetch ์‘๋‹ต Promise
58
- */
59
- async function fetchWithTimeout(url, options = {}, timeout = 5000) {
60
- console.log(`API ์š”์ฒญ: ${options.method || 'GET'} ${url}`);
61
 
62
- const controller = new AbortController();
63
- const id = setTimeout(() => controller.abort(), timeout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- try {
66
- const response = await fetch(url, {
67
- ...options,
68
- signal: controller.signal
69
- });
70
- clearTimeout(id);
71
- console.log(`API ์‘๋‹ต ์ƒํƒœ: ${response.status}`);
72
- return response;
73
- } catch (error) {
74
- clearTimeout(id);
75
- if (error.name === 'AbortError') {
76
- console.error(`API ์š”์ฒญ ํƒ€์ž„์•„์›ƒ: ${url}`);
77
- throw new Error('์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
78
  }
79
- console.error(`API ์š”์ฒญ ์‹คํŒจ: ${url}`, error);
80
- throw error;
81
- }
82
- }
83
-
84
- /**
85
- * ์žฅ์น˜ ์ƒํƒœ ๋กœ๋“œ ํ•จ์ˆ˜
86
- */
87
- async function loadDeviceStatus() {
88
- console.log('์žฅ์น˜ ์ƒํƒœ ๋กœ๋“œ ์‹œ์ž‘');
89
-
90
- // ์žฅ์น˜ ์ƒํƒœ ์ดˆ๊ธฐํ™”
91
- deviceStatus.innerHTML = '<div class="loading-device"><div class="spinner"></div><p>์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘...</p></div>';
92
- deviceList.innerHTML = '';
93
-
94
- try {
95
- console.log('์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ ์ค‘...');
96
- // ๋จผ์ € ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ
97
- const statusResponse = await fetchWithTimeout(`${DEVICE_SERVER_URL}/api/status`, {}, 3000);
98
 
99
- if (!statusResponse.ok) {
100
- throw new Error(`์„œ๋ฒ„ ์‘๋‹ต ์ฝ”๋“œ: ${statusResponse.status}`);
 
 
 
 
101
  }
102
 
103
- const statusData = await statusResponse.json();
104
- console.log(`์„œ๋ฒ„ ์ƒํƒœ: ${statusData.status}`);
105
-
106
- // ์žฅ์น˜ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
107
- console.log('์žฅ์น˜ ๋ชฉ๋ก ์š”์ฒญ ์ค‘...');
108
- const devicesResponse = await fetchWithTimeout(`${DEVICE_SERVER_URL}/api/devices`);
109
-
110
- if (!devicesResponse.ok) {
111
- throw new Error(`์žฅ์น˜ ๋ชฉ๋ก ์š”์ฒญ ์‹คํŒจ: ${devicesResponse.status}`);
112
  }
113
 
114
- const devicesData = await devicesResponse.json();
115
- console.log(`์žฅ์น˜ ๋ชฉ๋ก ์‘๋‹ต: ${devicesData.devices ? devicesData.devices.length : 0}๊ฐœ ์žฅ์น˜`);
116
-
117
- // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
118
- deviceStatus.innerHTML = '';
 
 
119
 
120
- // ์„œ๋ฒ„ ์ƒํƒœ ํ‘œ์‹œ
121
- const serverStatusDiv = document.createElement('div');
122
- serverStatusDiv.className = 'server-status success';
123
- serverStatusDiv.innerHTML = `<i class="fas fa-check-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ: ${statusData.status}`;
124
- deviceStatus.appendChild(serverStatusDiv);
 
 
125
 
126
- // ์žฅ์น˜ ๋ชฉ๋ก ํ‘œ์‹œ
127
- if (devicesData.devices && devicesData.devices.length > 0) {
128
- console.log('์žฅ์น˜ ๋ชฉ๋ก ํ™”๋ฉด์— ํ‘œ์‹œ');
129
-
130
- const deviceCountDiv = document.createElement('div');
131
- deviceCountDiv.className = 'device-count';
132
- deviceCountDiv.textContent = `์ด ${devicesData.devices.length}๊ฐœ ์žฅ์น˜ ์—ฐ๊ฒฐ๋จ`;
133
- deviceStatus.appendChild(deviceCountDiv);
134
-
135
- devicesData.devices.forEach(device => {
136
- const deviceItem = document.createElement('div');
137
- deviceItem.className = 'device-item';
138
-
139
- const deviceName = document.createElement('h3');
140
- deviceName.textContent = device.name;
141
- deviceItem.appendChild(deviceName);
142
-
143
- const deviceDetails = document.createElement('div');
144
- deviceDetails.className = 'device-details';
145
- deviceDetails.innerHTML = `
146
- <p><strong>์œ ํ˜•:</strong> ${device.type}</p>
147
- <p><strong>์ƒํƒœ:</strong> <span class="status-${device.status.toLowerCase()}">${device.status}</span></p>
148
- ${device.id ? `<p><strong>ID:</strong> ${device.id}</p>` : ''}
149
- `;
150
- deviceItem.appendChild(deviceDetails);
151
-
152
- deviceList.appendChild(deviceItem);
153
  });
154
- } else {
155
- console.log('์—ฐ๊ฒฐ๋œ ์žฅ์น˜ ์—†์Œ');
156
-
157
- const noDeviceMsg = document.createElement('div');
158
- noDeviceMsg.className = 'no-devices';
159
- noDeviceMsg.innerHTML = '<i class="fas fa-info-circle"></i> ํ˜„์žฌ ์—ฐ๊ฒฐ๋œ ์žฅ์น˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.';
160
- deviceList.appendChild(noDeviceMsg);
161
  }
162
- } catch (error) {
163
- console.error('์žฅ์น˜ ์ƒํƒœ ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
164
-
165
- deviceStatus.innerHTML = '';
166
- const errorDiv = document.createElement('div');
167
- errorDiv.className = 'server-status error';
168
-
169
- if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
170
- errorDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.';
171
- } else if (error.message.includes('Failed to fetch')) {
172
- errorDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.';
173
- } else {
174
- errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์˜ค๋ฅ˜: ${error.message}`;
175
  }
176
 
177
- deviceStatus.appendChild(errorDiv);
 
 
 
 
 
 
 
178
 
179
- // ์žฌ์‹œ๋„ ๋ฒ„ํŠผ
180
- const retryButton = document.createElement('button');
181
- retryButton.className = 'retry-button';
182
- retryButton.innerHTML = '<i class="fas fa-sync"></i> ๋‹ค์‹œ ์‹œ๋„';
183
- retryButton.addEventListener('click', loadDeviceStatus);
184
- deviceStatus.appendChild(retryButton);
185
 
186
- // ์žฅ์น˜ ๋ชฉ๋ก ์ดˆ๊ธฐํ™”
187
- deviceList.innerHTML = '';
188
- }
189
- }
190
-
191
- /**
192
- * ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
193
- */
194
- async function loadProgramsList() {
195
- console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์‹œ์ž‘');
196
-
197
- // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ดˆ๊ธฐํ™”
198
- programsList.innerHTML = '<div class="loading-programs"><div class="spinner"></div><p>ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์ค‘...</p></div>';
199
-
200
- try {
201
- console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก API ์š”์ฒญ ์ค‘...');
202
- const response = await fetchWithTimeout(`${DEVICE_SERVER_URL}/api/programs`);
203
 
204
- if (!response.ok) {
205
- throw new Error(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์š”์ฒญ ์‹คํŒจ: ${response.status}`);
 
 
 
 
 
 
 
 
 
 
206
  }
207
 
208
- const data = await response.json();
209
- console.log(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์‘๋‹ต: ${data.programs ? data.programs.length : 0}๊ฐœ ํ”„๋กœ๊ทธ๋žจ`);
210
-
211
- // ๋ชฉ๋ก ์ดˆ๊ธฐํ™”
212
- programsList.innerHTML = '';
213
 
214
- // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
215
- if (data.programs && data.programs.length > 0) {
216
- programsContainer.style.display = 'block';
 
 
 
 
 
 
217
 
218
- data.programs.forEach(program => {
219
- const programItem = document.createElement('div');
220
- programItem.className = 'program-item';
 
 
221
 
222
- const programName = document.createElement('h3');
223
- programName.textContent = program.name;
224
- programItem.appendChild(programName);
225
 
226
- if (program.description) {
227
- const programDesc = document.createElement('p');
228
- programDesc.className = 'program-description';
229
- programDesc.textContent = program.description;
230
- programItem.appendChild(programDesc);
231
- }
232
 
233
- const executeButton = document.createElement('button');
234
- executeButton.className = 'execute-btn';
235
- executeButton.innerHTML = '<i class="fas fa-play"></i> ์‹คํ–‰';
236
- executeButton.addEventListener('click', () => {
237
- executeProgram(program.id, program.name);
238
- });
239
- programItem.appendChild(executeButton);
240
 
241
- programsList.appendChild(programItem);
242
- });
243
- } else {
244
- programsContainer.style.display = 'block';
 
 
 
 
 
 
 
 
245
 
246
- const noProgramsMsg = document.createElement('div');
247
- noProgramsMsg.className = 'no-programs';
248
- noProgramsMsg.innerHTML = '<i class="fas fa-info-circle"></i> ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ์ด ์—†์Šต๋‹ˆ๋‹ค.';
249
- programsList.appendChild(noProgramsMsg);
 
 
 
 
250
  }
251
- } catch (error) {
252
- console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
 
 
 
253
 
254
- programsList.innerHTML = '';
255
- const errorDiv = document.createElement('div');
256
- errorDiv.className = 'error-message';
257
 
258
- if (error.message.includes('Failed to fetch')) {
259
- errorDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.';
260
- } else {
261
- errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜: ${error.message}`;
262
- }
263
 
264
- programsList.appendChild(errorDiv);
 
265
 
266
- // ์žฌ์‹œ๋„ ๋ฒ„ํŠผ
267
- const retryButton = document.createElement('button');
268
- retryButton.className = 'retry-button';
269
- retryButton.innerHTML = '<i class="fas fa-sync"></i> ๋‹ค์‹œ ์‹œ๋„';
270
- retryButton.addEventListener('click', loadProgramsList);
271
- programsList.appendChild(retryButton);
272
- }
273
- }
274
-
275
- /**
276
- * ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ํ•จ์ˆ˜
277
- * @param {string} programId - ํ”„๋กœ๊ทธ๋žจ ID
278
- * @param {string} programName - ํ”„๋กœ๊ทธ๋žจ ์ด๋ฆ„
279
- */
280
- async function executeProgram(programId, programName) {
281
- console.log(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ: ${programName} (ID: ${programId})`);
282
 
283
- // ํ”„๋กœ๊ทธ๋žจ ์š”์†Œ ์ฐพ๊ธฐ
284
- const programItems = document.querySelectorAll('.program-item');
285
- let programItem = null;
286
-
287
- for (const item of programItems) {
288
- if (item.querySelector('h3').textContent === programName) {
289
- programItem = item;
290
- break;
291
  }
292
- }
293
-
294
- if (!programItem) {
295
- console.error(`ํ”„๋กœ๊ทธ๋žจ ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: ${programName}`);
296
- return;
297
- }
298
-
299
- // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ถ”๊ฐ€
300
- const loadingIndicator = document.createElement('div');
301
- loadingIndicator.className = 'execute-loading';
302
- loadingIndicator.innerHTML = '<div class="spinner small"></div><span>์‹คํ–‰ ์ค‘...</span>';
303
- programItem.appendChild(loadingIndicator);
304
-
305
- try {
306
- console.log('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ API ์š”์ฒญ ์ค‘...');
307
- const response = await fetchWithTimeout(`${DEVICE_SERVER_URL}/api/programs/${programId}/execute`, {
308
- method: 'POST',
309
- headers: {
310
- 'Content-Type': 'application/json'
311
- },
312
- body: JSON.stringify({})
313
- });
314
 
315
- // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ œ๊ฑฐ
316
- programItem.removeChild(loadingIndicator);
 
317
 
318
- const data = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
- if (!response.ok || !data.success) {
321
- throw new Error(data.message || `์‹คํ–‰ ์‹คํŒจ (${response.status})`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
 
324
- console.log(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต: ${programName}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
- // ์„ฑ๊ณต ํ‘œ์‹œ
327
- const successIndicator = document.createElement('div');
328
- successIndicator.className = 'execute-success';
329
- successIndicator.innerHTML = '<i class="fas fa-check-circle"></i><span>์‹คํ–‰ ์™„๋ฃŒ</span>';
330
- programItem.appendChild(successIndicator);
 
 
331
 
332
- // 3์ดˆ ํ›„ ์„ฑ๊ณต ํ‘œ์‹œ ์ œ๊ฑฐ
333
- setTimeout(() => {
334
- if (programItem.contains(successIndicator)) {
335
- programItem.removeChild(successIndicator);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  }
337
- }, 3000);
338
-
339
- // ์‹œ์Šคํ…œ ์•Œ๋ฆผ ์ถ”๊ฐ€
340
- addSystemNotification(`ํ”„๋กœ๊ทธ๋žจ '${programName}' ์‹คํ–‰ ์„ฑ๊ณต: ${data.message}`);
341
- } catch (error) {
342
- console.error(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์˜ค๋ฅ˜ (${programName}):`, error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
- // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๊ฐ€ ์•„์ง ์žˆ์œผ๋ฉด ์ œ๊ฑฐ
345
- if (programItem.contains(loadingIndicator)) {
346
- programItem.removeChild(loadingIndicator);
 
347
  }
348
 
349
- // ์˜ค๋ฅ˜ ํ‘œ์‹œ
350
- const errorIndicator = document.createElement('div');
351
- errorIndicator.className = 'execute-error';
352
- errorIndicator.innerHTML = `<i class="fas fa-exclamation-circle"></i><span>์‹คํ–‰ ์‹คํŒจ</span>`;
353
- programItem.appendChild(errorIndicator);
354
 
355
- // 3์ดˆ ํ›„ ์˜ค๋ฅ˜ ํ‘œ์‹œ ์ œ๊ฑฐ
356
- setTimeout(() => {
357
- if (programItem.contains(errorIndicator)) {
358
- programItem.removeChild(errorIndicator);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
- }, 3000);
361
-
362
- // ์‹œ์Šคํ…œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
363
- addErrorMessage(`ํ”„๋กœ๊ทธ๋žจ '${programName}' ์‹คํ–‰ ์‹คํŒจ: ${error.message}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  }
365
- }
366
 
367
- // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
368
  document.addEventListener('DOMContentLoaded', function() {
369
- console.log('์žฅ์น˜ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”');
370
 
371
- // ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ดˆ๊ธฐํ™” (DOM ์š”์†Œ๊ฐ€ ์ค€๋น„๋œ ํ›„)
372
  setTimeout(() => {
373
- initDeviceElements();
374
  }, 100);
375
  });
 
1
  /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์žฅ์น˜ ์ œ์–ด JavaScript
3
  */
4
 
5
+ // ์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ
6
+ const DeviceControl = {
7
+ // ์žฅ์น˜ ์ œ์–ด ์ƒํƒœ
8
+ isConnected: false,
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
+ // ํƒญ ์ „ํ™˜
81
+ if (this.elements.deviceTab) {
82
+ this.elements.deviceTab.addEventListener('click', () => {
83
+ console.log('์žฅ์น˜ ์ œ์–ด ํƒญ ํด๋ฆญ');
84
+ this.switchToDeviceTab();
85
+ });
 
 
 
 
 
86
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ // ์„œ๋ฒ„ ์—ฐ๊ฒฐ
89
+ if (this.elements.connectDeviceServerBtn) {
90
+ this.elements.connectDeviceServerBtn.addEventListener('click', () => {
91
+ console.log('์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๋ฒ„ํŠผ ํด๋ฆญ');
92
+ this.connectServer();
93
+ });
94
  }
95
 
96
+ // ์—”ํ„ฐ ํ‚ค๋กœ ์—ฐ๊ฒฐ
97
+ if (this.elements.deviceServerUrlInput) {
98
+ this.elements.deviceServerUrlInput.addEventListener('keydown', (event) => {
99
+ if (event.key === 'Enter') {
100
+ console.log('์žฅ์น˜ ์„œ๋ฒ„ URL ์ž…๋ ฅ ํ•„๋“œ์—์„œ ์—”ํ„ฐ ํ‚ค ๊ฐ์ง€');
101
+ event.preventDefault();
102
+ this.connectServer();
103
+ }
104
+ });
105
  }
106
 
107
+ // ์žฅ์น˜ ์ƒํƒœ ํ™•์ธ
108
+ if (this.elements.checkDeviceStatusBtn) {
109
+ this.elements.checkDeviceStatusBtn.addEventListener('click', () => {
110
+ console.log('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ');
111
+ this.checkDeviceStatus();
112
+ });
113
+ }
114
 
115
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ
116
+ if (this.elements.getProgramsBtn) {
117
+ this.elements.getProgramsBtn.addEventListener('click', () => {
118
+ console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ ํด๋ฆญ');
119
+ this.loadProgramsList();
120
+ });
121
+ }
122
 
123
+ // ํ”„๋กœ๊ทธ๋žจ ์„ ํƒ ๋ณ€๊ฒฝ
124
+ if (this.elements.programSelectDropdown) {
125
+ this.elements.programSelectDropdown.addEventListener('change', (event) => {
126
+ console.log(`ํ”„๋กœ๊ทธ๋žจ ์„ ํƒ ๋ณ€๊ฒฝ: ${event.target.value}`);
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 ngrokUrl = this.elements.deviceServerUrlInput.value.trim();
163
+
164
+ if (!ngrokUrl) {
165
+ this.updateConnectionStatus('error', '์„œ๋ฒ„ URL์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
166
+ console.error('์„œ๋ฒ„ URL์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.');
167
+ return;
168
  }
169
 
170
+ // ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘ UI ์—…๋ฐ์ดํŠธ
171
+ this.elements.connectDeviceServerBtn.disabled = true;
172
+ this.updateConnectionStatus('connecting', '์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๋„ ์ค‘...');
 
 
173
 
174
+ try {
175
+ console.log(`์žฅ๏ฟฝ๏ฟฝ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๋„: ${ngrokUrl}`);
176
+
177
+ // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœํ•˜์—ฌ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ
178
+ const response = await AppUtils.fetchWithTimeout('/api/device/status', {
179
+ method: 'GET'
180
+ }, 10000); // 10์ดˆ ํƒ€์ž„์•„์›ƒ
181
+
182
+ const data = await response.json();
183
 
184
+ if (response.ok && data.success) {
185
+ // ์—ฐ๊ฒฐ ์„ฑ๊ณต
186
+ console.log('์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต:', data);
187
+ this.isConnected = true;
188
+ this.updateConnectionStatus('connected', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! ์ƒํƒœ: ${data.server_status || '์ •์ƒ'}`);
189
 
190
+ // ๊ธฐ๋Šฅ UI ํ™œ์„ฑํ™”
191
+ this.elements.deviceBasicFunctions.classList.add('active');
192
+ this.elements.deviceProgramControl.classList.add('active');
193
 
194
+ // ์žฅ์น˜ ์ƒํƒœ ์ž๋™ ์ฒดํฌ
195
+ this.checkDeviceStatus();
 
 
 
 
196
 
197
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ž๋™ ๋กœ๋“œ
198
+ this.loadProgramsList();
 
 
 
 
 
199
 
200
+ // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
201
+ AppUtils.addSystemNotification(`์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ฑ๊ณต! (${ngrokUrl})`);
202
+ } else {
203
+ // ์—ฐ๊ฒฐ ์‹คํŒจ
204
+ console.error('์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ:', data);
205
+ this.isConnected = false;
206
+ this.updateConnectionStatus('error', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ: ${data.error || '์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜'}`);
207
+ }
208
+ } catch (error) {
209
+ // ์˜ˆ์™ธ ๋ฐœ์ƒ
210
+ console.error('์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
211
+ this.isConnected = false;
212
 
213
+ if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
214
+ this.updateConnectionStatus('error', '์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.');
215
+ } else {
216
+ this.updateConnectionStatus('error', `์„œ๋ฒ„ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜: ${error.message}`);
217
+ }
218
+ } finally {
219
+ // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
220
+ this.elements.connectDeviceServerBtn.disabled = false;
221
  }
222
+ },
223
+
224
+ // ์—ฐ๊ฒฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
225
+ updateConnectionStatus: function(status, message) {
226
+ const statusElement = this.elements.deviceConnectionStatus;
227
 
228
+ // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
229
+ statusElement.classList.remove('connected', 'disconnected', 'error', 'connecting');
 
230
 
231
+ // ์ƒํƒœ์— ๋”ฐ๋ผ ํด๋ž˜์Šค ์ถ”๊ฐ€
232
+ statusElement.classList.add(status);
 
 
 
233
 
234
+ // ๋ฉ”์‹œ์ง€ ์—…๋ฐ์ดํŠธ
235
+ statusElement.textContent = message;
236
 
237
+ console.log(`์—ฐ๊ฒฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ: ${status} - ${message}`);
238
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
+ // ์žฅ์น˜ ์ƒํƒœ ํ™•์ธ
241
+ checkDeviceStatus: async function() {
242
+ if (!this.isConnected) {
243
+ this.elements.deviceStatusResult.value = '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.';
244
+ console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
245
+ return;
 
 
246
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
+ // ์ƒํƒœ ํ™•์ธ ์ค‘ UI ์—…๋ฐ์ดํŠธ
249
+ this.elements.checkDeviceStatusBtn.disabled = true;
250
+ this.elements.deviceStatusResult.value = '์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘...';
251
 
252
+ try {
253
+ console.log('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ ์ „์†ก');
254
+
255
+ // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
256
+ const response = await AppUtils.fetchWithTimeout('/api/device/status', {
257
+ method: 'GET'
258
+ });
259
+
260
+ const data = await response.json();
261
+
262
+ if (response.ok && data.success) {
263
+ // ์ƒํƒœ ํ™•์ธ ์„ฑ๊ณต
264
+ console.log('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์„ฑ๊ณต:', data);
265
+ this.isStatusChecked = true;
266
+ this.elements.deviceStatusResult.value = JSON.stringify(data, null, 2);
267
+ } else {
268
+ // ์ƒํƒœ ํ™•์ธ ์‹คํŒจ
269
+ console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์‹คํŒจ:', data);
270
+ this.elements.deviceStatusResult.value = `์ƒํƒœ ํ™•์ธ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`;
271
+ }
272
+ } catch (error) {
273
+ // ์˜ˆ์™ธ ๋ฐœ์ƒ
274
+ console.error('์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
275
+ this.elements.deviceStatusResult.value = `์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`;
276
+ } finally {
277
+ // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
278
+ this.elements.checkDeviceStatusBtn.disabled = false;
279
+ }
280
+ },
281
+
282
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ
283
+ loadProgramsList: async function() {
284
+ if (!this.isConnected) {
285
+ this.showProgramsError('์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
286
+ console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
287
+ return;
288
+ }
289
+
290
+ // ์ด๋ฏธ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€
291
+ if (this.isLoadingPrograms) {
292
+ console.log('์ด๋ฏธ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋”ฉ ์ค‘');
293
+ return;
294
+ }
295
 
296
+ // ๋กœ๋”ฉ ์ค‘ UI ์—…๋ฐ์ดํŠธ
297
+ this.isLoadingPrograms = true;
298
+ this.elements.getProgramsBtn.disabled = true;
299
+ this.elements.programsList.innerHTML = `
300
+ <div class="loading-message">
301
+ ${AppUtils.createLoadingSpinner()} ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์ค‘...
302
+ </div>
303
+ `;
304
+
305
+ try {
306
+ console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์š”์ฒญ ์ „์†ก');
307
+
308
+ // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
309
+ const response = await AppUtils.fetchWithTimeout('/api/device/programs', {
310
+ method: 'GET'
311
+ });
312
+
313
+ const data = await response.json();
314
+
315
+ if (response.ok && data.success) {
316
+ // ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต
317
+ console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต:', data);
318
+ this.programsList = data.programs || [];
319
+
320
+ // ๋ชฉ๋ก ํ‘œ์‹œ
321
+ this.displayProgramsList();
322
+
323
+ // ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
324
+ this.updateProgramsDropdown();
325
+
326
+ // ์‹คํ–‰ ๋ฒ„ํŠผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
327
+ this.updateExecuteButton();
328
+ } else {
329
+ // ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ
330
+ console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:', data);
331
+ this.showProgramsError(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`);
332
+ }
333
+ } catch (error) {
334
+ // ์˜ˆ์™ธ ๋ฐœ์ƒ
335
+ console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
336
+ this.showProgramsError(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
337
+ } finally {
338
+ // ๋กœ๋”ฉ ์ƒํƒœ ๋ฐ ๋ฒ„ํŠผ ์ƒํƒœ ๋ณต์›
339
+ this.isLoadingPrograms = false;
340
+ this.elements.getProgramsBtn.disabled = false;
341
+ }
342
+ },
343
+
344
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
345
+ displayProgramsList: function() {
346
+ const programsListElement = this.elements.programsList;
347
+
348
+ if (!this.programsList || this.programsList.length === 0) {
349
+ programsListElement.innerHTML = `
350
+ <div class="no-programs-message">
351
+ <i class="fas fa-info-circle"></i> ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ์ด ์—†์Šต๋‹ˆ๋‹ค.
352
+ </div>
353
+ `;
354
+ return;
355
  }
356
 
357
+ // ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
358
+ let html = `
359
+ <table class="program-list">
360
+ <thead>
361
+ <tr>
362
+ <th>์ด๋ฆ„</th>
363
+ <th>์„ค๋ช…</th>
364
+ <th>๊ฒฝ๋กœ</th>
365
+ </tr>
366
+ </thead>
367
+ <tbody>
368
+ `;
369
+
370
+ // ํ”„๋กœ๊ทธ๋žจ ํ•ญ๋ชฉ ์ƒ์„ฑ
371
+ this.programsList.forEach(program => {
372
+ html += `
373
+ <tr>
374
+ <td>${AppUtils.escapeHtml(program.name || '์•Œ ์ˆ˜ ์—†์Œ')}</td>
375
+ <td>${AppUtils.escapeHtml(program.description || '-')}</td>
376
+ <td>${AppUtils.escapeHtml(program.path || '-')}</td>
377
+ </tr>
378
+ `;
379
+ });
380
 
381
+ html += `
382
+ </tbody>
383
+ </table>
384
+ <div style="margin-top: 10px; font-size: 0.9em; color: #666;">
385
+ ์ด ${this.programsList.length}๊ฐœ ํ”„๋กœ๊ทธ๋žจ
386
+ </div>
387
+ `;
388
 
389
+ programsListElement.innerHTML = html;
390
+ },
391
+
392
+ // ํ”„๋กœ๊ทธ๋žจ ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
393
+ updateProgramsDropdown: function() {
394
+ const dropdown = this.elements.programSelectDropdown;
395
+
396
+ // ๊ธฐ์กด ์˜ต์…˜ ์ œ๊ฑฐ
397
+ dropdown.innerHTML = '';
398
+
399
+ // ๊ธฐ๋ณธ ์˜ต์…˜ ์ถ”๊ฐ€
400
+ const defaultOption = document.createElement('option');
401
+ defaultOption.value = '';
402
+ defaultOption.textContent = this.programsList.length > 0
403
+ ? '-- ์‹คํ–‰ํ•  ํ”„๋กœ๊ทธ๋žจ ์„ ํƒ --'
404
+ : '-- ํ”„๋กœ๊ทธ๋žจ ์—†์Œ --';
405
+ dropdown.appendChild(defaultOption);
406
+
407
+ // ํ”„๋กœ๊ทธ๋žจ ์˜ต์…˜ ์ถ”๊ฐ€
408
+ this.programsList.forEach(program => {
409
+ const option = document.createElement('option');
410
+ option.value = program.id || '';
411
+ option.textContent = program.name || '์•Œ ์ˆ˜ ์—†์Œ';
412
+
413
+ // ์„ค๋ช…์ด ์žˆ์œผ๋ฉด ๊ด„ํ˜ธ๋กœ ์ถ”๊ฐ€
414
+ if (program.description) {
415
+ option.textContent += ` (${program.description})`;
416
  }
417
+
418
+ dropdown.appendChild(option);
419
+ });
420
+ },
421
+
422
+ // ์‹คํ–‰ ๋ฒ„ํŠผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
423
+ updateExecuteButton: function() {
424
+ const dropdown = this.elements.programSelectDropdown;
425
+ const executeBtn = this.elements.executeProgramBtn;
426
+
427
+ // ์„ ํƒ๋œ ํ”„๋กœ๊ทธ๋žจ์ด ์žˆ์„ ๋•Œ๋งŒ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
428
+ executeBtn.disabled = !dropdown.value;
429
+ },
430
+
431
+ // ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
432
+ executeProgram: async function(programId) {
433
+ if (!this.isConnected) {
434
+ this.showExecuteResult('error', '์˜ค๋ฅ˜: ๋จผ์ € ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
435
+ console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์•ˆ๋จ');
436
+ return;
437
+ }
438
 
439
+ if (!programId) {
440
+ this.showExecuteResult('error', '์˜ค๋ฅ˜: ์‹คํ–‰ํ•  ํ”„๋กœ๊ทธ๋žจ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.');
441
+ console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜: ํ”„๋กœ๊ทธ๋žจ ID ์—†์Œ');
442
+ return;
443
  }
444
 
445
+ // ์‹คํ–‰ ์ค‘ UI ์—…๋ฐ์ดํŠธ
446
+ this.elements.executeProgramBtn.disabled = true;
447
+ this.showExecuteResult('loading', 'ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘...');
 
 
448
 
449
+ try {
450
+ console.log(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์ „์†ก: ${programId}`);
451
+
452
+ // ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ
453
+ const response = await AppUtils.fetchWithTimeout(`/api/device/programs/${programId}/execute`, {
454
+ method: 'POST',
455
+ headers: {
456
+ 'Content-Type': 'application/json'
457
+ },
458
+ body: JSON.stringify({})
459
+ }, 15000); // 15์ดˆ ํƒ€์ž„์•„์›ƒ (์‹คํ–‰์— ์‹œ๊ฐ„์ด ๋” ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Œ)
460
+
461
+ const data = await response.json();
462
+
463
+ if (response.ok && data.success) {
464
+ // ์‹คํ–‰ ์„ฑ๊ณต
465
+ console.log('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต:', data);
466
+ this.showExecuteResult('success', `์‹คํ–‰ ์„ฑ๊ณต: ${data.message || 'ํ”„๋กœ๊ทธ๋žจ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'}`);
467
+
468
+ // ์‹œ์Šคํ…œ ์•Œ๋ฆผ
469
+ AppUtils.addSystemNotification(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต: ${this.getSelectedProgramName()}`);
470
+ } else {
471
+ // ์‹คํ–‰ ์‹คํŒจ
472
+ console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹คํŒจ:', data);
473
+ this.showExecuteResult('error', `์‹คํ–‰ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`);
474
  }
475
+ } catch (error) {
476
+ // ์˜ˆ์™ธ ๋ฐœ์ƒ
477
+ console.error('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
478
+
479
+ if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
480
+ this.showExecuteResult('error', 'ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์‹œ๊ฐ„ ์ดˆ๊ณผ. ์„œ๋ฒ„ ์‘๋‹ต์ด ์—†์Šต๋‹ˆ๋‹ค.');
481
+ } else {
482
+ this.showExecuteResult('error', `ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${error.message}`);
483
+ }
484
+ } finally {
485
+ // ๋ฒ„ํŠผ ๋‹ค์‹œ ํ™œ์„ฑํ™”
486
+ this.elements.executeProgramBtn.disabled = false;
487
+ }
488
+ },
489
+
490
+ // ์„ ํƒ๋œ ํ”„๋กœ๊ทธ๋žจ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ
491
+ getSelectedProgramName: function() {
492
+ const dropdown = this.elements.programSelectDropdown;
493
+ const selectedOption = dropdown.options[dropdown.selectedIndex];
494
+ return selectedOption ? selectedOption.textContent : '์•Œ ์ˆ˜ ์—†๋Š” ํ”„๋กœ๊ทธ๋žจ';
495
+ },
496
+
497
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์˜ค๋ฅ˜ ํ‘œ์‹œ
498
+ showProgramsError: function(errorMessage) {
499
+ this.elements.programsList.innerHTML = `
500
+ <div class="error-message">
501
+ <i class="fas fa-exclamation-circle"></i> ${errorMessage}
502
+ <button class="retry-button" id="retryLoadProgramsBtn">
503
+ <i class="fas fa-sync"></i> ๋‹ค์‹œ ์‹œ๋„
504
+ </button>
505
+ </div>
506
+ `;
507
+
508
+ // ์žฌ์‹œ๋„ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
509
+ document.getElementById('retryLoadProgramsBtn').addEventListener('click', () => {
510
+ console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์žฌ์‹œ๋„ ๋ฒ„ํŠผ ํด๋ฆญ');
511
+ this.loadProgramsList();
512
+ });
513
+ },
514
+
515
+ // ์‹คํ–‰ ๊ฒฐ๊ณผ ํ‘œ์‹œ
516
+ showExecuteResult: function(status, message) {
517
+ const resultElement = this.elements.executeResult;
518
+
519
+ // ๋ชจ๋“  ์ƒํƒœ ํด๋ž˜์Šค ์ œ๊ฑฐ
520
+ resultElement.classList.remove('success', 'error', 'warning');
521
+
522
+ // ๋‚ด์šฉ ์ดˆ๊ธฐํ™”
523
+ resultElement.innerHTML = '';
524
+
525
+ // ์ƒํƒœ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ
526
+ switch (status) {
527
+ case 'success':
528
+ resultElement.classList.add('success');
529
+ resultElement.innerHTML = `<i class="fas fa-check-circle"></i> ${message}`;
530
+ break;
531
+ case 'error':
532
+ resultElement.classList.add('error');
533
+ resultElement.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${message}`;
534
+ break;
535
+ case 'warning':
536
+ resultElement.classList.add('warning');
537
+ resultElement.innerHTML = `<i class="fas fa-exclamation-triangle"></i> ${message}`;
538
+ break;
539
+ case 'loading':
540
+ resultElement.innerHTML = `${AppUtils.createLoadingSpinner()} ${message}`;
541
+ break;
542
+ default:
543
+ resultElement.textContent = message;
544
+ }
545
  }
546
+ };
547
 
548
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์™„๋ฃŒ ์‹œ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
549
  document.addEventListener('DOMContentLoaded', function() {
550
+ console.log('์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ ๋กœ๋“œ๋จ');
551
 
552
+ // DOM์ด ์™„์ „ํžˆ ๋กœ๋“œ๋œ ํ›„ ์•ฝ๊ฐ„์˜ ์ง€์—ฐ์„ ๋‘๊ณ  ์ดˆ๊ธฐํ™”
553
  setTimeout(() => {
554
+ DeviceControl.init();
555
  }, 100);
556
  });
app/static/js/app.js CHANGED
@@ -1,12 +1,15 @@
1
  /**
2
  * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI JavaScript
 
3
  */
4
 
5
  // DOM ์š”์†Œ
6
  const chatTab = document.getElementById('chatTab');
7
  const docsTab = document.getElementById('docsTab');
 
8
  const chatSection = document.getElementById('chatSection');
9
  const docsSection = document.getElementById('docsSection');
 
10
  const chatMessages = document.getElementById('chatMessages');
11
  const userInput = document.getElementById('userInput');
12
  const sendButton = document.getElementById('sendButton');
@@ -37,14 +40,17 @@ let isRecording = false;
37
  // ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ํ•จ์ˆ˜
38
  async function checkAppStatus() {
39
  try {
 
40
  const response = await fetch('/api/status');
41
  if (!response.ok) {
 
42
  return false;
43
  }
44
  const data = await response.json();
 
45
  return data.ready;
46
  } catch (error) {
47
- console.error('Status check failed:', error);
48
  return false;
49
  }
50
  }
@@ -55,16 +61,17 @@ async function checkAppStatus() {
55
  async function loadLLMs() {
56
  try {
57
  // API ์š”์ฒญ
58
- const response = await fetch('/api/llm');
59
-
60
- if (!response.ok) {
61
- throw new Error(`HTTP error! status: ${response.status}`);
62
- }
63
 
64
  const data = await response.json();
65
  supportedLLMs = data.supported_llms;
66
  currentLLM = data.current_llm.id;
67
 
 
 
68
  // LLM ์„ ํƒ ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
69
  llmSelect.innerHTML = '';
70
  supportedLLMs.forEach(llm => {
@@ -89,7 +96,8 @@ async function loadLLMs() {
89
  async function changeLLM(llmId) {
90
  try {
91
  // API ์š”์ฒญ
92
- const response = await fetch('/api/llm', {
 
93
  method: 'POST',
94
  headers: {
95
  'Content-Type': 'application/json'
@@ -97,20 +105,16 @@ async function changeLLM(llmId) {
97
  body: JSON.stringify({ llm_id: llmId })
98
  });
99
 
100
- if (!response.ok) {
101
- throw new Error(`HTTP error! status: ${response.status}`);
102
- }
103
-
104
  const data = await response.json();
105
 
106
  if (data.success) {
107
  currentLLM = llmId;
108
  updateCurrentLLMInfo(data.current_llm);
109
- console.log(`LLM์ด ${data.current_llm.name}(์œผ)๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`);
110
 
111
  // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
112
  const systemMessage = `LLM์ด ${data.current_llm.name}(์œผ)๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋ธ: ${data.current_llm.model}`;
113
- addSystemNotification(systemMessage);
114
  } else if (data.error) {
115
  console.error('LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜:', data.error);
116
  alert(`LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜: ${data.error}`);
@@ -131,123 +135,42 @@ function updateCurrentLLMInfo(llmInfo) {
131
  }
132
  }
133
 
134
- /**
135
- * ์‹œ์Šคํ…œ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
136
- * @param {string} message - ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€
137
- */
138
- function addSystemNotification(message) {
139
- const messageDiv = document.createElement('div');
140
- messageDiv.classList.add('message', 'system');
141
-
142
- const contentDiv = document.createElement('div');
143
- contentDiv.classList.add('message-content');
144
-
145
- const messageP = document.createElement('p');
146
- messageP.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
147
- contentDiv.appendChild(messageP);
148
-
149
- messageDiv.appendChild(contentDiv);
150
- chatMessages.appendChild(messageDiv);
151
-
152
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
153
- chatMessages.scrollTop = chatMessages.scrollHeight;
154
- }
155
-
156
- // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™”
157
- document.addEventListener('DOMContentLoaded', () => {
158
- // ์•ฑ ์ƒํƒœ ํ™•์ธ (๋กœ๋”ฉ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ)
159
- if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) {
160
- // ์•ฑ ์ƒํƒœ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
161
- const statusInterval = setInterval(async () => {
162
- const isReady = await checkAppStatus();
163
- if (isReady) {
164
- clearInterval(statusInterval);
165
- console.log('์•ฑ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
166
-
167
- // ์•ฑ์ด ์ค€๋น„๋˜๋ฉด LLM ๋ชฉ๋ก ๋กœ๋“œ
168
- loadLLMs();
169
- }
170
- }, 5000);
171
- }
172
-
173
- // ํƒญ ์ „ํ™˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
174
- chatTab.addEventListener('click', () => {
175
- switchTab('chat');
176
- });
177
-
178
- docsTab.addEventListener('click', () => {
179
- switchTab('docs');
180
- loadDocuments();
181
- });
182
-
183
- // LLM ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
184
- llmSelect.addEventListener('change', (event) => {
185
- changeLLM(event.target.value);
186
- });
187
-
188
- // ๋ฉ”์‹œ์ง€ ์ „์†ก ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
189
- sendButton.addEventListener('click', sendMessage);
190
- userInput.addEventListener('keydown', (event) => {
191
- if (event.key === 'Enter' && !event.shiftKey) {
192
- event.preventDefault();
193
- sendMessage();
194
- }
195
- });
196
-
197
- // ์Œ์„ฑ ์ธ์‹ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
198
- micButton.addEventListener('click', startRecording);
199
- stopRecordingButton.addEventListener('click', stopRecording);
200
-
201
- // ๋ฌธ์„œ ์—…๋กœ๋“œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
202
- documentFile.addEventListener('change', (event) => {
203
- if (event.target.files.length > 0) {
204
- fileName.textContent = event.target.files[0].name;
205
- } else {
206
- fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
207
- }
208
- });
209
-
210
- uploadForm.addEventListener('submit', (event) => {
211
- event.preventDefault();
212
- uploadDocument();
213
- });
214
-
215
- // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
216
- refreshDocsButton.addEventListener('click', loadDocuments);
217
 
218
- // ์ž๋™ ์ž…๋ ฅ ํ•„๋“œ ํฌ๊ธฐ ์กฐ์ •
219
- userInput.addEventListener('input', adjustTextareaHeight);
 
 
 
 
 
220
 
221
- // ์ดˆ๊ธฐ ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ
222
- if (docsSection.classList.contains('active')) {
223
- loadDocuments();
224
- }
225
- });
226
-
227
- /**
228
- * ํƒญ ์ „ํ™˜ ํ•จ์ˆ˜
229
- * @param {string} tabName - ํ™œ์„ฑํ™”ํ•  ํƒญ ์ด๋ฆ„ ('chat' ๋˜๋Š” 'docs')
230
- */
231
- function switchTab(tabName) {
232
  if (tabName === 'chat') {
233
  chatTab.classList.add('active');
234
- docsTab.classList.remove('active');
235
  chatSection.classList.add('active');
236
- docsSection.classList.remove('active');
237
  } else if (tabName === 'docs') {
238
- chatTab.classList.remove('active');
239
  docsTab.classList.add('active');
240
- chatSection.classList.remove('active');
241
  docsSection.classList.add('active');
 
 
 
 
 
242
  }
243
  }
244
 
245
- /**
246
- * ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
247
- */
248
  async function sendMessage() {
249
  const message = userInput.value.trim();
250
- if (!message) return;
 
 
 
 
 
251
 
252
  // UI ์—…๋ฐ์ดํŠธ
253
  addMessage(message, 'user');
@@ -259,7 +182,8 @@ async function sendMessage() {
259
 
260
  try {
261
  // API ์š”์ฒญ
262
- const response = await fetch('/api/chat', {
 
263
  method: 'POST',
264
  headers: {
265
  'Content-Type': 'application/json'
@@ -270,29 +194,28 @@ async function sendMessage() {
270
  })
271
  });
272
 
273
- if (!response.ok) {
274
- throw new Error(`HTTP error! status: ${response.status}`);
275
- }
276
-
277
  const data = await response.json();
 
278
 
279
  // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
280
  removeLoadingMessage(loadingMessageId);
281
 
282
  // ์‘๋‹ต ํ‘œ์‹œ
283
  if (data.error) {
284
- addErrorMessage(data.error);
 
285
  } else {
286
  // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
287
  if (data.llm) {
 
288
  updateCurrentLLMInfo(data.llm);
289
  }
290
  addMessage(data.answer, 'bot', null, data.sources);
291
  }
292
  } catch (error) {
293
- console.error('Error:', error);
294
  removeLoadingMessage(loadingMessageId);
295
- addErrorMessage('์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
296
  }
297
  }
298
 
@@ -300,9 +223,13 @@ async function sendMessage() {
300
  * ์Œ์„ฑ ๋…น์Œ ์‹œ์ž‘ ํ•จ์ˆ˜
301
  */
302
  async function startRecording() {
303
- if (isRecording) return;
 
 
 
304
 
305
  try {
 
306
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
307
  isRecording = true;
308
  audioChunks = [];
@@ -310,19 +237,24 @@ async function startRecording() {
310
  mediaRecorder = new MediaRecorder(stream);
311
 
312
  mediaRecorder.addEventListener('dataavailable', (event) => {
313
- if (event.data.size > 0) audioChunks.push(event.data);
 
 
 
314
  });
315
 
316
- mediaRecorder.addEventListener('stop', sendAudioMessage);
 
 
 
317
 
318
  // ๋…น์Œ ์‹œ์ž‘
319
  mediaRecorder.start();
 
320
 
321
  // UI ์—…๋ฐ์ดํŠธ
322
  micButton.style.display = 'none';
323
  recordingStatus.classList.remove('hidden');
324
-
325
- console.log('๋…น์Œ ์‹œ์ž‘๋จ');
326
  } catch (error) {
327
  console.error('์Œ์„ฑ ๋…น์Œ ๊ถŒํ•œ์„ ์–ป์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:', error);
328
  alert('๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');
@@ -333,23 +265,30 @@ async function startRecording() {
333
  * ์Œ์„ฑ ๋…น์Œ ์ค‘์ง€ ํ•จ์ˆ˜
334
  */
335
  function stopRecording() {
336
- if (!isRecording || !mediaRecorder) return;
 
 
 
337
 
 
338
  mediaRecorder.stop();
339
  isRecording = false;
340
 
341
  // UI ์—…๋ฐ์ดํŠธ
342
  micButton.style.display = 'flex';
343
  recordingStatus.classList.add('hidden');
344
-
345
- console.log('๋…น์Œ ์ค‘์ง€๋จ');
346
  }
347
 
348
  /**
349
  * ๋…น์Œ๋œ ์˜ค๋””์˜ค ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
350
  */
351
  async function sendAudioMessage() {
352
- if (audioChunks.length === 0) return;
 
 
 
 
 
353
 
354
  // ์˜ค๋””์˜ค Blob ์ƒ์„ฑ
355
  const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
@@ -364,32 +303,33 @@ async function sendAudioMessage() {
364
  // ํ˜„์žฌ ์„ ํƒ๋œ LLM ์ถ”๊ฐ€
365
  formData.append('llm_id', currentLLM);
366
 
 
367
  // API ์š”์ฒญ
368
- const response = await fetch('/api/voice', {
369
  method: 'POST',
370
  body: formData
371
- });
372
-
373
- if (!response.ok) {
374
- throw new Error(`HTTP error! status: ${response.status}`);
375
- }
376
 
377
  const data = await response.json();
 
378
 
379
  // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
380
  removeLoadingMessage(loadingMessageId);
381
 
382
  // ์‘๋‹ต ํ‘œ์‹œ
383
  if (data.error) {
384
- addErrorMessage(data.error);
 
385
  } else {
386
  // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
387
  if (data.llm) {
 
388
  updateCurrentLLMInfo(data.llm);
389
  }
390
 
391
  // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€(์Œ์„ฑ ํ…์ŠคํŠธ) ์ถ”๊ฐ€
392
  if (data.transcription) {
 
393
  addMessage(data.transcription, 'user');
394
  }
395
 
@@ -397,9 +337,9 @@ async function sendAudioMessage() {
397
  addMessage(data.answer, 'bot', data.transcription, data.sources);
398
  }
399
  } catch (error) {
400
- console.error('Error:', error);
401
  removeLoadingMessage(loadingMessageId);
402
- addErrorMessage('์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
403
  }
404
  }
405
 
@@ -408,10 +348,13 @@ async function sendAudioMessage() {
408
  */
409
  async function uploadDocument() {
410
  if (documentFile.files.length === 0) {
 
411
  alert('ํŒŒ์ผ์„ ์„ ํƒํ•ด ์ฃผ์„ธ์š”.');
412
  return;
413
  }
414
 
 
 
415
  // UI ์—…๋ฐ์ดํŠธ
416
  uploadStatus.classList.remove('hidden');
417
  uploadStatus.className = 'upload-status';
@@ -423,21 +366,26 @@ async function uploadDocument() {
423
  formData.append('document', documentFile.files[0]);
424
 
425
  // API ์š”์ฒญ
426
- const response = await fetch('/api/upload', {
 
427
  method: 'POST',
428
  body: formData
429
- });
430
 
431
  const data = await response.json();
 
432
 
433
  // ์‘๋‹ต ์ฒ˜๋ฆฌ
434
  if (data.error) {
 
435
  uploadStatus.className = 'upload-status error';
436
  uploadStatus.textContent = `์˜ค๋ฅ˜: ${data.error}`;
437
  } else if (data.warning) {
 
438
  uploadStatus.className = 'upload-status warning';
439
  uploadStatus.textContent = data.message;
440
  } else {
 
441
  uploadStatus.className = 'upload-status success';
442
  uploadStatus.textContent = data.message;
443
 
@@ -449,7 +397,7 @@ async function uploadDocument() {
449
  fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
450
  }
451
  } catch (error) {
452
- console.error('Error:', error);
453
  uploadStatus.className = 'upload-status error';
454
  uploadStatus.textContent = '์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.';
455
  } finally {
@@ -461,6 +409,8 @@ async function uploadDocument() {
461
  * ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
462
  */
463
  async function loadDocuments() {
 
 
464
  // UI ์—…๋ฐ์ดํŠธ
465
  docsList.querySelector('tbody').innerHTML = '';
466
  docsLoading.classList.remove('hidden');
@@ -468,18 +418,19 @@ async function loadDocuments() {
468
 
469
  try {
470
  // API ์š”์ฒญ
471
- const response = await fetch('/api/documents');
472
-
473
- if (!response.ok) {
474
- throw new Error(`HTTP error! status: ${response.status}`);
475
- }
476
 
477
  const data = await response.json();
 
478
 
479
  // ์‘๋‹ต ์ฒ˜๋ฆฌ
480
  docsLoading.classList.add('hidden');
481
 
482
  if (!data.documents || data.documents.length === 0) {
 
483
  noDocsMessage.classList.remove('hidden');
484
  return;
485
  }
@@ -504,7 +455,7 @@ async function loadDocuments() {
504
  tbody.appendChild(row);
505
  });
506
  } catch (error) {
507
- console.error('Error:', error);
508
  docsLoading.classList.add('hidden');
509
  noDocsMessage.classList.remove('hidden');
510
  noDocsMessage.querySelector('p').textContent = '๋ฌธ์„œ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.';
@@ -519,6 +470,8 @@ async function loadDocuments() {
519
  * @param {Array|null} sources - ์†Œ์Šค ์ •๋ณด ๋ฐฐ์—ด (์„ ํƒ ์‚ฌํ•ญ)
520
  */
521
  function addMessage(text, sender, transcription = null, sources = null) {
 
 
522
  const messageDiv = document.createElement('div');
523
  messageDiv.classList.add('message', sender);
524
 
@@ -540,6 +493,7 @@ function addMessage(text, sender, transcription = null, sources = null) {
540
 
541
  // ์†Œ์Šค ์ •๋ณด ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
542
  if (sources && sources.length > 0 && sender === 'bot') {
 
543
  const sourcesDiv = document.createElement('div');
544
  sourcesDiv.classList.add('sources');
545
 
@@ -571,6 +525,7 @@ function addMessage(text, sender, transcription = null, sources = null) {
571
  * @returns {string} ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
572
  */
573
  function addLoadingMessage() {
 
574
  const id = 'loading-' + Date.now();
575
  const messageDiv = document.createElement('div');
576
  messageDiv.classList.add('message', 'bot');
@@ -597,36 +552,13 @@ function addLoadingMessage() {
597
  * @param {string} id - ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
598
  */
599
  function removeLoadingMessage(id) {
 
600
  const loadingMessage = document.getElementById(id);
601
  if (loadingMessage) {
602
  loadingMessage.remove();
603
  }
604
  }
605
 
606
- /**
607
- * ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
608
- * @param {string} errorText - ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
609
- */
610
- function addErrorMessage(errorText) {
611
- const messageDiv = document.createElement('div');
612
- messageDiv.classList.add('message', 'system');
613
-
614
- const contentDiv = document.createElement('div');
615
- contentDiv.classList.add('message-content');
616
- contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
617
- contentDiv.style.color = 'var(--error-color)';
618
-
619
- const errorP = document.createElement('p');
620
- errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`;
621
- contentDiv.appendChild(errorP);
622
-
623
- messageDiv.appendChild(contentDiv);
624
- chatMessages.appendChild(messageDiv);
625
-
626
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
627
- chatMessages.scrollTop = chatMessages.scrollHeight;
628
- }
629
-
630
  /**
631
  * textarea ๋†’์ด ์ž๋™ ์กฐ์ • ํ•จ์ˆ˜
632
  */
@@ -634,3 +566,99 @@ function adjustTextareaHeight() {
634
  userInput.style.height = 'auto';
635
  userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
636
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
  * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI JavaScript
3
+ * ๋ฉ”์ธ ํŒŒ์ผ - ์ฝ”์–ด ๋ฐ ์žฅ์น˜ ์ œ์–ด ๋ชจ๋“ˆ๊ณผ ํ†ตํ•ฉ
4
  */
5
 
6
  // DOM ์š”์†Œ
7
  const chatTab = document.getElementById('chatTab');
8
  const docsTab = document.getElementById('docsTab');
9
+ const deviceTab = document.getElementById('deviceTab'); // ์žฅ์น˜ ์ œ์–ด ํƒญ ์ถ”๊ฐ€
10
  const chatSection = document.getElementById('chatSection');
11
  const docsSection = document.getElementById('docsSection');
12
+ const deviceSection = document.getElementById('deviceSection'); // ์žฅ์น˜ ์ œ์–ด ์„น์…˜ ์ถ”๊ฐ€
13
  const chatMessages = document.getElementById('chatMessages');
14
  const userInput = document.getElementById('userInput');
15
  const sendButton = document.getElementById('sendButton');
 
40
  // ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ํ•จ์ˆ˜
41
  async function checkAppStatus() {
42
  try {
43
+ console.log('์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ ์ „์†ก');
44
  const response = await fetch('/api/status');
45
  if (!response.ok) {
46
+ console.error(`์•ฑ ์ƒํƒœ ํ™•์ธ ์‹คํŒจ: ${response.status}`);
47
  return false;
48
  }
49
  const data = await response.json();
50
+ console.log(`์•ฑ ์ƒํƒœ ํ™•์ธ ๊ฒฐ๊ณผ: ${data.ready ? '์ค€๋น„๋จ' : '์ค€๋น„ ์•ˆ๋จ'}`);
51
  return data.ready;
52
  } catch (error) {
53
+ console.error('์•ฑ ์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
54
  return false;
55
  }
56
  }
 
61
  async function loadLLMs() {
62
  try {
63
  // API ์š”์ฒญ
64
+ console.log('LLM ๋ชฉ๋ก ์š”์ฒญ ์ „์†ก');
65
+ const response = await AppUtils.fetchWithTimeout('/api/llm', {
66
+ method: 'GET'
67
+ });
 
68
 
69
  const data = await response.json();
70
  supportedLLMs = data.supported_llms;
71
  currentLLM = data.current_llm.id;
72
 
73
+ console.log(`LLM ๋ชฉ๋ก ๋กœ๋“œ ์„ฑ๊ณต: ${supportedLLMs.length}๊ฐœ ๋ชจ๋ธ`);
74
+
75
  // LLM ์„ ํƒ ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
76
  llmSelect.innerHTML = '';
77
  supportedLLMs.forEach(llm => {
 
96
  async function changeLLM(llmId) {
97
  try {
98
  // API ์š”์ฒญ
99
+ console.log(`LLM ๋ณ€๊ฒฝ ์š”์ฒญ: ${llmId}`);
100
+ const response = await AppUtils.fetchWithTimeout('/api/llm', {
101
  method: 'POST',
102
  headers: {
103
  'Content-Type': 'application/json'
 
105
  body: JSON.stringify({ llm_id: llmId })
106
  });
107
 
 
 
 
 
108
  const data = await response.json();
109
 
110
  if (data.success) {
111
  currentLLM = llmId;
112
  updateCurrentLLMInfo(data.current_llm);
113
+ console.log(`LLM ๋ณ€๊ฒฝ ์„ฑ๊ณต: ${data.current_llm.name}`);
114
 
115
  // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
116
  const systemMessage = `LLM์ด ${data.current_llm.name}(์œผ)๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋ธ: ${data.current_llm.model}`;
117
+ AppUtils.addSystemNotification(systemMessage);
118
  } else if (data.error) {
119
  console.error('LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜:', data.error);
120
  alert(`LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜: ${data.error}`);
 
135
  }
136
  }
137
 
138
+ // ํƒญ ์ „ํ™˜ ํ•จ์ˆ˜
139
+ function switchTab(tabName) {
140
+ console.log(`ํƒญ ์ „ํ™˜: ${tabName}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ // ๋ชจ๋“  ํƒญ๊ณผ ์„น์…˜ ๋น„ํ™œ์„ฑํ™”
143
+ chatTab.classList.remove('active');
144
+ docsTab.classList.remove('active');
145
+ deviceTab.classList.remove('active');
146
+ chatSection.classList.remove('active');
147
+ docsSection.classList.remove('active');
148
+ deviceSection.classList.remove('active');
149
 
150
+ // ์„ ํƒํ•œ ํƒญ๊ณผ ์„น์…˜ ํ™œ์„ฑํ™”
 
 
 
 
 
 
 
 
 
 
151
  if (tabName === 'chat') {
152
  chatTab.classList.add('active');
 
153
  chatSection.classList.add('active');
 
154
  } else if (tabName === 'docs') {
 
155
  docsTab.classList.add('active');
 
156
  docsSection.classList.add('active');
157
+ // ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ
158
+ loadDocuments();
159
+ } else if (tabName === 'device') {
160
+ deviceTab.classList.add('active');
161
+ deviceSection.classList.add('active');
162
  }
163
  }
164
 
165
+ // ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
 
 
166
  async function sendMessage() {
167
  const message = userInput.value.trim();
168
+ if (!message) {
169
+ console.log('๋ฉ”์‹œ์ง€๊ฐ€ ๋น„์–ด์žˆ์–ด ์ „์†กํ•˜์ง€ ์•Š์Œ');
170
+ return;
171
+ }
172
+
173
+ console.log('๋ฉ”์‹œ์ง€ ์ „์†ก ์‹œ์ž‘');
174
 
175
  // UI ์—…๋ฐ์ดํŠธ
176
  addMessage(message, 'user');
 
182
 
183
  try {
184
  // API ์š”์ฒญ
185
+ console.log(`/api/chat API ํ˜ธ์ถœ: ${message.substring(0, 30)}${message.length > 30 ? '...' : ''}`);
186
+ const response = await AppUtils.fetchWithTimeout('/api/chat', {
187
  method: 'POST',
188
  headers: {
189
  'Content-Type': 'application/json'
 
194
  })
195
  });
196
 
 
 
 
 
197
  const data = await response.json();
198
+ console.log('API ์‘๋‹ต ์ˆ˜์‹  ์™„๋ฃŒ');
199
 
200
  // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
201
  removeLoadingMessage(loadingMessageId);
202
 
203
  // ์‘๋‹ต ํ‘œ์‹œ
204
  if (data.error) {
205
+ console.error(`API ์˜ค๋ฅ˜ ์‘๋‹ต: ${data.error}`);
206
+ AppUtils.addErrorMessage(data.error);
207
  } else {
208
  // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
209
  if (data.llm) {
210
+ console.log(`LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ: ${data.llm.name}`);
211
  updateCurrentLLMInfo(data.llm);
212
  }
213
  addMessage(data.answer, 'bot', null, data.sources);
214
  }
215
  } catch (error) {
216
+ console.error('๋ฉ”์‹œ์ง€ ์ „์†ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
217
  removeLoadingMessage(loadingMessageId);
218
+ AppUtils.addErrorMessage('์˜ค๋ฅ˜๊ฐ€ ๋ฐœ๏ฟฝ๏ฟฝ๏ฟฝํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
219
  }
220
  }
221
 
 
223
  * ์Œ์„ฑ ๋…น์Œ ์‹œ์ž‘ ํ•จ์ˆ˜
224
  */
225
  async function startRecording() {
226
+ if (isRecording) {
227
+ console.log('์ด๋ฏธ ๋…น์Œ ์ค‘');
228
+ return;
229
+ }
230
 
231
  try {
232
+ console.log('๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ ์š”์ฒญ');
233
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
234
  isRecording = true;
235
  audioChunks = [];
 
237
  mediaRecorder = new MediaRecorder(stream);
238
 
239
  mediaRecorder.addEventListener('dataavailable', (event) => {
240
+ if (event.data.size > 0) {
241
+ console.log('์˜ค๋””์˜ค ์ฒญํฌ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ');
242
+ audioChunks.push(event.data);
243
+ }
244
  });
245
 
246
+ mediaRecorder.addEventListener('stop', () => {
247
+ console.log('๋…น์Œ ์ค‘์ง€ ์ด๋ฒคํŠธ - ์˜ค๋””์˜ค ๋ฉ”์‹œ์ง€ ์ „์†ก');
248
+ sendAudioMessage();
249
+ });
250
 
251
  // ๋…น์Œ ์‹œ์ž‘
252
  mediaRecorder.start();
253
+ console.log('๋…น์Œ ์‹œ์ž‘๋จ');
254
 
255
  // UI ์—…๋ฐ์ดํŠธ
256
  micButton.style.display = 'none';
257
  recordingStatus.classList.remove('hidden');
 
 
258
  } catch (error) {
259
  console.error('์Œ์„ฑ ๋…น์Œ ๊ถŒํ•œ์„ ์–ป์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:', error);
260
  alert('๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');
 
265
  * ์Œ์„ฑ ๋…น์Œ ์ค‘์ง€ ํ•จ์ˆ˜
266
  */
267
  function stopRecording() {
268
+ if (!isRecording || !mediaRecorder) {
269
+ console.log('๋…น์Œ ์ค‘์ด ์•„๋‹˜');
270
+ return;
271
+ }
272
 
273
+ console.log('๋…น์Œ ์ค‘์ง€ ์š”์ฒญ');
274
  mediaRecorder.stop();
275
  isRecording = false;
276
 
277
  // UI ์—…๋ฐ์ดํŠธ
278
  micButton.style.display = 'flex';
279
  recordingStatus.classList.add('hidden');
 
 
280
  }
281
 
282
  /**
283
  * ๋…น์Œ๋œ ์˜ค๋””์˜ค ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
284
  */
285
  async function sendAudioMessage() {
286
+ if (audioChunks.length === 0) {
287
+ console.log('์˜ค๋””์˜ค ์ฒญํฌ๊ฐ€ ์—†์Œ');
288
+ return;
289
+ }
290
+
291
+ console.log(`์˜ค๋””์˜ค ๋ฉ”์‹œ์ง€ ์ „์†ก ์‹œ์ž‘: ${audioChunks.length}๊ฐœ ์ฒญํฌ`);
292
 
293
  // ์˜ค๋””์˜ค Blob ์ƒ์„ฑ
294
  const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
 
303
  // ํ˜„์žฌ ์„ ํƒ๋œ LLM ์ถ”๊ฐ€
304
  formData.append('llm_id', currentLLM);
305
 
306
+ console.log('/api/voice API ํ˜ธ์ถœ');
307
  // API ์š”์ฒญ
308
+ const response = await AppUtils.fetchWithTimeout('/api/voice', {
309
  method: 'POST',
310
  body: formData
311
+ }, 30000); // ์Œ์„ฑ ์ฒ˜๋ฆฌ๋Š” ๋” ๊ธด ํƒ€์ž„์•„์›ƒ
 
 
 
 
312
 
313
  const data = await response.json();
314
+ console.log('์Œ์„ฑ API ์‘๋‹ต ์ˆ˜์‹  ์™„๋ฃŒ');
315
 
316
  // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
317
  removeLoadingMessage(loadingMessageId);
318
 
319
  // ์‘๋‹ต ํ‘œ์‹œ
320
  if (data.error) {
321
+ console.error(`์Œ์„ฑ API ์˜ค๋ฅ˜ ์‘๋‹ต: ${data.error}`);
322
+ AppUtils.addErrorMessage(data.error);
323
  } else {
324
  // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
325
  if (data.llm) {
326
+ console.log(`LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ: ${data.llm.name}`);
327
  updateCurrentLLMInfo(data.llm);
328
  }
329
 
330
  // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€(์Œ์„ฑ ํ…์ŠคํŠธ) ์ถ”๊ฐ€
331
  if (data.transcription) {
332
+ console.log(`์Œ์„ฑ ์ธ์‹ ๊ฒฐ๊ณผ: ${data.transcription.substring(0, 30)}${data.transcription.length > 30 ? '...' : ''}`);
333
  addMessage(data.transcription, 'user');
334
  }
335
 
 
337
  addMessage(data.answer, 'bot', data.transcription, data.sources);
338
  }
339
  } catch (error) {
340
+ console.error('์Œ์„ฑ ๋ฉ”์‹œ์ง€ ์ „์†ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
341
  removeLoadingMessage(loadingMessageId);
342
+ AppUtils.addErrorMessage('์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
343
  }
344
  }
345
 
 
348
  */
349
  async function uploadDocument() {
350
  if (documentFile.files.length === 0) {
351
+ console.log('์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ');
352
  alert('ํŒŒ์ผ์„ ์„ ํƒํ•ด ์ฃผ์„ธ์š”.');
353
  return;
354
  }
355
 
356
+ console.log(`๋ฌธ์„œ ์—…๋กœ๋“œ ์‹œ์ž‘: ${documentFile.files[0].name}`);
357
+
358
  // UI ์—…๋ฐ์ดํŠธ
359
  uploadStatus.classList.remove('hidden');
360
  uploadStatus.className = 'upload-status';
 
366
  formData.append('document', documentFile.files[0]);
367
 
368
  // API ์š”์ฒญ
369
+ console.log('/api/upload API ํ˜ธ์ถœ');
370
+ const response = await AppUtils.fetchWithTimeout('/api/upload', {
371
  method: 'POST',
372
  body: formData
373
+ }, 20000); // ์—…๋กœ๋“œ๋Š” ๋” ๊ธด ํƒ€์ž„์•„์›ƒ
374
 
375
  const data = await response.json();
376
+ console.log('์—…๋กœ๋“œ API ์‘๋‹ต ์ˆ˜์‹  ์™„๋ฃŒ');
377
 
378
  // ์‘๋‹ต ์ฒ˜๋ฆฌ
379
  if (data.error) {
380
+ console.error(`์—…๋กœ๋“œ ์˜ค๋ฅ˜: ${data.error}`);
381
  uploadStatus.className = 'upload-status error';
382
  uploadStatus.textContent = `์˜ค๋ฅ˜: ${data.error}`;
383
  } else if (data.warning) {
384
+ console.warn(`์—…๋กœ๋“œ ๊ฒฝ๊ณ : ${data.message}`);
385
  uploadStatus.className = 'upload-status warning';
386
  uploadStatus.textContent = data.message;
387
  } else {
388
+ console.log(`์—…๋กœ๋“œ ์„ฑ๊ณต: ${data.message}`);
389
  uploadStatus.className = 'upload-status success';
390
  uploadStatus.textContent = data.message;
391
 
 
397
  fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
398
  }
399
  } catch (error) {
400
+ console.error('๋ฌธ์„œ ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
401
  uploadStatus.className = 'upload-status error';
402
  uploadStatus.textContent = '์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.';
403
  } finally {
 
409
  * ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
410
  */
411
  async function loadDocuments() {
412
+ console.log('๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ์‹œ์ž‘');
413
+
414
  // UI ์—…๋ฐ์ดํŠธ
415
  docsList.querySelector('tbody').innerHTML = '';
416
  docsLoading.classList.remove('hidden');
 
418
 
419
  try {
420
  // API ์š”์ฒญ
421
+ console.log('/api/documents API ํ˜ธ์ถœ');
422
+ const response = await AppUtils.fetchWithTimeout('/api/documents', {
423
+ method: 'GET'
424
+ });
 
425
 
426
  const data = await response.json();
427
+ console.log(`๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ์„ฑ๊ณต: ${data.documents ? data.documents.length : 0}๊ฐœ ๋ฌธ์„œ`);
428
 
429
  // ์‘๋‹ต ์ฒ˜๋ฆฌ
430
  docsLoading.classList.add('hidden');
431
 
432
  if (!data.documents || data.documents.length === 0) {
433
+ console.log('๋ฌธ์„œ ์—†์Œ');
434
  noDocsMessage.classList.remove('hidden');
435
  return;
436
  }
 
455
  tbody.appendChild(row);
456
  });
457
  } catch (error) {
458
+ console.error('๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
459
  docsLoading.classList.add('hidden');
460
  noDocsMessage.classList.remove('hidden');
461
  noDocsMessage.querySelector('p').textContent = '๋ฌธ์„œ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.';
 
470
  * @param {Array|null} sources - ์†Œ์Šค ์ •๋ณด ๋ฐฐ์—ด (์„ ํƒ ์‚ฌํ•ญ)
471
  */
472
  function addMessage(text, sender, transcription = null, sources = null) {
473
+ console.log(`๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€: sender=${sender}, length=${text ? text.length : 0}`);
474
+
475
  const messageDiv = document.createElement('div');
476
  messageDiv.classList.add('message', sender);
477
 
 
493
 
494
  // ์†Œ์Šค ์ •๋ณด ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
495
  if (sources && sources.length > 0 && sender === 'bot') {
496
+ console.log(`์†Œ์Šค ์ •๋ณด ์ถ”๊ฐ€: ${sources.length}๊ฐœ ์†Œ์Šค`);
497
  const sourcesDiv = document.createElement('div');
498
  sourcesDiv.classList.add('sources');
499
 
 
525
  * @returns {string} ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
526
  */
527
  function addLoadingMessage() {
528
+ console.log('๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€');
529
  const id = 'loading-' + Date.now();
530
  const messageDiv = document.createElement('div');
531
  messageDiv.classList.add('message', 'bot');
 
552
  * @param {string} id - ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
553
  */
554
  function removeLoadingMessage(id) {
555
+ console.log(`๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ: ${id}`);
556
  const loadingMessage = document.getElementById(id);
557
  if (loadingMessage) {
558
  loadingMessage.remove();
559
  }
560
  }
561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  /**
563
  * textarea ๋†’์ด ์ž๋™ ์กฐ์ • ํ•จ์ˆ˜
564
  */
 
566
  userInput.style.height = 'auto';
567
  userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
568
  }
569
+
570
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™”
571
+ document.addEventListener('DOMContentLoaded', () => {
572
+ console.log('๋ฉ”์ธ UI ์ดˆ๊ธฐํ™” ์ค‘...');
573
+
574
+ // ์•ฑ ์ƒํƒœ ํ™•์ธ (๋กœ๋”ฉ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ)
575
+ if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) {
576
+ // ์•ฑ ์ƒํƒœ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
577
+ const statusInterval = setInterval(async () => {
578
+ const isReady = await checkAppStatus();
579
+ if (isReady) {
580
+ clearInterval(statusInterval);
581
+ console.log('์•ฑ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
582
+
583
+ // ์•ฑ์ด ์ค€๋น„๋˜๋ฉด LLM ๋ชฉ๋ก ๋กœ๋“œ
584
+ loadLLMs();
585
+ }
586
+ }, 5000);
587
+ }
588
+
589
+ // ํƒญ ์ „ํ™˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
590
+ chatTab.addEventListener('click', () => {
591
+ console.log('์ฑ„ํŒ… ํƒญ ํด๋ฆญ');
592
+ switchTab('chat');
593
+ });
594
+
595
+ docsTab.addEventListener('click', () => {
596
+ console.log('๋ฌธ์„œ ๊ด€๋ฆฌ ํƒญ ํด๋ฆญ');
597
+ switchTab('docs');
598
+ });
599
+
600
+ // ์žฅ์น˜ ์ œ์–ด ํƒญ์€ DeviceControl ๋ชจ๋“ˆ์—์„œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋กํ•จ
601
+
602
+ // LLM ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
603
+ llmSelect.addEventListener('change', (event) => {
604
+ console.log(`LLM ๋ณ€๊ฒฝ: ${event.target.value}`);
605
+ changeLLM(event.target.value);
606
+ });
607
+
608
+ // ๋ฉ”์‹œ์ง€ ์ „์†ก ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
609
+ sendButton.addEventListener('click', () => {
610
+ console.log('๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฒ„ํŠผ ํด๋ฆญ');
611
+ sendMessage();
612
+ });
613
+
614
+ userInput.addEventListener('keydown', (event) => {
615
+ if (event.key === 'Enter' && !event.shiftKey) {
616
+ console.log('ํ…์ŠคํŠธ ์ž…๋ ฅ์—์„œ ์—”ํ„ฐ ํ‚ค ๊ฐ์ง€');
617
+ event.preventDefault();
618
+ sendMessage();
619
+ }
620
+ });
621
+
622
+ // ์Œ์„ฑ ์ธ์‹ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
623
+ micButton.addEventListener('click', () => {
624
+ console.log('๋งˆ์ดํฌ ๋ฒ„ํŠผ ํด๋ฆญ');
625
+ startRecording();
626
+ });
627
+
628
+ stopRecordingButton.addEventListener('click', () => {
629
+ console.log('๋…น์Œ ์ค‘์ง€ ๋ฒ„ํŠผ ํด๋ฆญ');
630
+ stopRecording();
631
+ });
632
+
633
+ // ๋ฌธ์„œ ์—…๋กœ๋“œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
634
+ documentFile.addEventListener('change', (event) => {
635
+ console.log('ํŒŒ์ผ ์„ ํƒ ๋ณ€๊ฒฝ');
636
+ if (event.target.files.length > 0) {
637
+ fileName.textContent = event.target.files[0].name;
638
+ } else {
639
+ fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
640
+ }
641
+ });
642
+
643
+ uploadForm.addEventListener('submit', (event) => {
644
+ console.log('๋ฌธ์„œ ์—…๋กœ๋“œ ํผ ์ œ์ถœ');
645
+ event.preventDefault();
646
+ uploadDocument();
647
+ });
648
+
649
+ // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
650
+ refreshDocsButton.addEventListener('click', () => {
651
+ console.log('๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ ํด๋ฆญ');
652
+ loadDocuments();
653
+ });
654
+
655
+ // ์ž๋™ ์ž…๋ ฅ ํ•„๋“œ ํฌ๊ธฐ ์กฐ์ •
656
+ userInput.addEventListener('input', adjustTextareaHeight);
657
+
658
+ // ์ดˆ๊ธฐ ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ
659
+ if (docsSection.classList.contains('active')) {
660
+ loadDocuments();
661
+ }
662
+
663
+ console.log('๋ฉ”์ธ UI ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
664
+ });
app/templates/index.html CHANGED
@@ -6,6 +6,7 @@
6
  <title>RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡</title>
7
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
8
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
 
9
  </head>
10
  <body>
11
  <div class="container">
@@ -28,6 +29,7 @@
28
  <div class="tabs">
29
  <button id="chatTab" class="tab active">๋Œ€ํ™”</button>
30
  <button id="docsTab" class="tab">๋ฌธ์„œ๊ด€๋ฆฌ</button>
 
31
  </div>
32
  </header>
33
 
@@ -119,6 +121,42 @@
119
  </div>
120
  </div>
121
  </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </main>
123
 
124
  <footer>
@@ -130,6 +168,8 @@
130
  </footer>
131
  </div>
132
 
 
 
133
  <script src="{{ url_for('static', filename='js/app.js') }}"></script>
134
  </body>
135
  </html>
 
6
  <title>RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡</title>
7
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
8
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
9
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/device-style.css') }}">
10
  </head>
11
  <body>
12
  <div class="container">
 
29
  <div class="tabs">
30
  <button id="chatTab" class="tab active">๋Œ€ํ™”</button>
31
  <button id="docsTab" class="tab">๋ฌธ์„œ๊ด€๋ฆฌ</button>
32
+ <button id="deviceTab" class="tab">์žฅ์น˜์ œ์–ด</button>
33
  </div>
34
  </header>
35
 
 
121
  </div>
122
  </div>
123
  </section>
124
+ <!-- ์žฅ์น˜ ์ œ์–ด ํƒญ -->
125
+ <section id="deviceSection" class="tab-content">
126
+ <div class="device-connection">
127
+ <h3>1. ์žฅ์น˜ ์„œ๋ฒ„ ์—ฐ๊ฒฐ</h3>
128
+ <div class="device-connection-form">
129
+ <input type="text" id="deviceServerUrlInput" placeholder="LocalPCAgent Ngrok URL ์ž…๋ ฅ (https://xxxx-xx-xxx-xxx.ngrok-free.app ํ˜•์‹)">
130
+ <button id="connectDeviceServerBtn">์—ฐ๊ฒฐ</button>
131
+ </div>
132
+ <div id="deviceConnectionStatus" class="connection-status disconnected">์—ฐ๊ฒฐ ์ƒํƒœ: ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Œ</div>
133
+ </div>
134
+
135
+ <div id="deviceBasicFunctions" class="device-functions">
136
+ <h3>2. ๊ธฐ๋ณธ ๊ธฐ๋Šฅ</h3>
137
+ <div class="function-buttons">
138
+ <button id="checkDeviceStatusBtn">์žฅ์น˜ ์ƒํƒœ ํ™•์ธ</button>
139
+ </div>
140
+ <textarea id="deviceStatusResult" class="device-status-result" readonly></textarea>
141
+ </div>
142
+
143
+ <div id="deviceProgramControl" class="program-control">
144
+ <h3>3. ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰</h3>
145
+ <div class="function-buttons">
146
+ <button id="getProgramsBtn">ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ</button>
147
+ </div>
148
+ <div id="programsList" class="program-list-container">
149
+ <div class="no-programs-message">ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</div>
150
+ </div>
151
+ <div class="program-select-container">
152
+ <select id="programSelectDropdown">
153
+ <option value="">-- ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ํ›„ ์„ ํƒ --</option>
154
+ </select>
155
+ </div>
156
+ <button id="executeProgramBtn" class="execute-btn" disabled>์„ ํƒํ•œ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰</button>
157
+ <div id="executeResult" class="execute-result"></div>
158
+ </div>
159
+ </section>
160
  </main>
161
 
162
  <footer>
 
168
  </footer>
169
  </div>
170
 
171
+ <script src="{{ url_for('static', filename='js/app-core.js') }}"></script>
172
+ <script src="{{ url_for('static', filename='js/app-device.js') }}"></script>
173
  <script src="{{ url_for('static', filename='js/app.js') }}"></script>
174
  </body>
175
  </html>
docs/project_plan.md ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gradio RAG ์ฑ—๋ด‡ & LocalPCAgent ์ œ์–ด ์›น์•ฑ ํ†ตํ•ฉ ํ”„๋กœ์ ํŠธ ๊ณ„ํš
2
+
3
+ ## 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š”
4
+
5
+ ๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ๊ธฐ์กด์— ๊ตฌํ˜„๋œ RAG ์ฑ—๋ด‡ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ LocalPCAgent ์ œ์–ด ๊ธฐ๋Šฅ์„ ๊ฐ€์ง„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ•˜๋‚˜์˜ ํ†ตํ•ฉ๋œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๊ฒฐํ•ฉํ•˜๋Š” ์ž‘์—…์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋‹จ์ผ ์›น ์ธํ„ฐํŽ˜์ด์Šค ๋‚ด์—์„œ RAG ์ฑ—๋ด‡๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋ฉด์„œ ๋™์‹œ์— ์›๊ฒฉ PC์˜ ํ”„๋กœ๊ทธ๋žจ์„ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋Š” ๋“ฑ์˜ ์ œ์–ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
6
+
7
+ ## 2. ํ”„๋กœ์ ํŠธ ํ˜„์žฌ ์ƒํƒœ
8
+
9
+ ### ์™„๋ฃŒ๋œ ์ž‘์—…
10
+ - [x] ๋ฐฑ์—”๋“œ API ํ™•์ธ ๋ฐ ์ค€๋น„ (app_device_routes.py)
11
+ - [x] HTML/CSS UI ์š”์†Œ ์ถ”๊ฐ€ (ํ…œํ”Œ๋ฆฟ ์ˆ˜์ •, CSS ์ถ”๊ฐ€)
12
+ - [x] JavaScript ์ฝ”์–ด ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ชจ๋“ˆ ์ƒ์„ฑ (app-core.js)
13
+ - [x] ์žฅ์น˜ ์ œ์–ด UI ๋ชจ๋“ˆ ์ƒ์„ฑ (app-device.js)
14
+ - [x] ๋ฉ”์ธ JavaScript ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ (app.js)
15
+
16
+ ### ๊ตฌ์„ฑ ์š”์†Œ
17
+ - **Flask ๋ฐฑ์—”๋“œ**
18
+ - app_revised.py: ๋ฉ”์ธ Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
19
+ - app_routes.py: RAG ์ฑ—๋ด‡ ๊ด€๋ จ API ๋ผ์šฐํŠธ
20
+ - app_device_routes.py: ์žฅ์น˜ ์ œ์–ด ๊ด€๋ จ API ๋ผ์šฐํŠธ
21
+ - init_retriever.py: RAG ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ๊ด€๋ จ ์ฝ”๋“œ
22
+
23
+ - **์›น ํ”„๋ก ํŠธ์—”๋“œ**
24
+ - index.html: ๋ฉ”์ธ UI ํ…œํ”Œ๋ฆฟ (์ฑ„ํŒ…, ๋ฌธ์„œ ๊ด€๋ฆฌ, ์žฅ์น˜ ์ œ์–ด ํƒญ ํฌํ•จ)
25
+ - app-core.js: ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ JavaScript ๋ชจ๋“ˆ
26
+ - app-device.js: ์žฅ์น˜ ์ œ์–ด ๊ด€๋ จ JavaScript ๋ชจ๋“ˆ
27
+ - app.js: ๋ฉ”์ธ JavaScript ์ฝ”๋“œ
28
+ - device-style.css: ์žฅ์น˜ ์ œ์–ด UI ๊ด€๋ จ ์Šคํƒ€์ผ
29
+
30
+ ## 3. ๊ธฐ๋Šฅ ์„ค๋ช…
31
+
32
+ ### RAG ์ฑ—๋ด‡ ๊ธฐ๋Šฅ
33
+ - ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์งˆ์˜์‘๋‹ต
34
+ - ์Œ์„ฑ ์ธ์‹์„ ํ†ตํ•œ ์งˆ์˜์‘๋‹ต
35
+ - ๋ฌธ์„œ ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ
36
+ - ๋‹ค์–‘ํ•œ LLM ๋ชจ๋ธ ์„ ํƒ ๊ฐ€๋Šฅ (OpenAI, DeepSeek ๋“ฑ)
37
+
38
+ ### LocalPCAgent ์ œ์–ด ๊ธฐ๋Šฅ
39
+ - ์›๊ฒฉ PC ์ƒํƒœ ํ™•์ธ
40
+ - ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ
41
+ - ์›๊ฒฉ์œผ๋กœ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
42
+
43
+ ## 4. ๊ธฐ์ˆ  ์Šคํƒ
44
+
45
+ ### ๋ฐฑ์—”๋“œ
46
+ - Flask: ์›น ์„œ๋ฒ„ ๋ฐ API ์ œ๊ณต
47
+ - OpenAI/DeepSeek: LLM ๋ชจ๋ธ ํ†ต์‹ 
48
+ - VITO STT: ์Œ์„ฑ-ํ…์ŠคํŠธ ๋ณ€ํ™˜
49
+ - ์ž„๋ฒ ๋”ฉ/๊ฒ€์ƒ‰: RAG ๊ธฐ๋Šฅ ๊ตฌํ˜„
50
+
51
+ ### ํ”„๋ก ํŠธ์—”๋“œ
52
+ - HTML/CSS/JavaScript: ๊ธฐ๋ณธ ์›น ์ธํ„ฐํŽ˜์ด์Šค
53
+ - Fetch API: ๋ฐฑ์—”๋“œ ํ†ต์‹ 
54
+ - MediaRecorder API: ์˜ค๋””์˜ค ๋…น์Œ
55
+
56
+ ## 5. ํ†ตํ•ฉ ์•„ํ‚คํ…์ฒ˜
57
+
58
+ ### ํ†ตํ•ฉ ๋ฐฉ์‹
59
+ ์ด ํ”„๋กœ์ ํŠธ๋Š” "Flask ๋ฐฑ์—”๋“œ + HTML/JavaScript ํ”„๋ก ํŠธ์—”๋“œ" ๊ตฌ์กฐ๋กœ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. RAG ์ฑ—๋ด‡๊ณผ ์žฅ์น˜ ์ œ์–ด ๊ธฐ๋Šฅ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ†ตํ•ฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:
60
+
61
+ 1. **๋ฐฑ์—”๋“œ ํ†ตํ•ฉ**
62
+ - app_revised.py์—์„œ RAG ์ฑ—๋ด‡ ๋ผ์šฐํŠธ(app_routes.py)์™€ ์žฅ์น˜ ์ œ์–ด ๋ผ์šฐํŠธ(app_device_routes.py)๋ฅผ ๋ชจ๋‘ ๋“ฑ๋ก
63
+ - ์žฅ์น˜ ์ œ์–ด๋Š” ๋ฐฑ์—”๋“œ์—์„œ LocalPCAgent์™€ ํ†ต์‹  (ํ”„๋ก์‹œ ํŒจํ„ด)
64
+
65
+ 2. **ํ”„๋ก ํŠธ์—”๋“œ ํ†ตํ•ฉ**
66
+ - ํƒญ ๊ธฐ๋ฐ˜ UI๋กœ ๊ธฐ๋Šฅ ๋ถ„๋ฆฌ (์ฑ„ํŒ…, ๋ฌธ์„œ ๊ด€๋ฆฌ, ์žฅ์น˜ ์ œ์–ด)
67
+ - ๋ชจ๋“ˆํ™”๋œ JavaScript ํŒŒ์ผ๋กœ ๊ฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„
68
+ - ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋Š” app-core.js์— ํ†ตํ•ฉ
69
+
70
+ ### ๋ฐ์ดํ„ฐ ํ๋ฆ„
71
+ - ์‚ฌ์šฉ์ž -(์š”์ฒญ)-> ์›น UI -(API ํ˜ธ์ถœ)-> Flask ๋ฐฑ์—”๋“œ -(ํ”„๋ก์‹œ ์š”์ฒญ)-> LocalPCAgent/LLM
72
+ - LocalPCAgent/LLM -(์‘๋‹ต)-> Flask ๋ฐฑ์—”๋“œ -(JSON ์‘๋‹ต)-> ์›น UI -(ํ‘œ์‹œ)-> ์‚ฌ์šฉ์ž
73
+
74
+ ## 6. ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ
75
+
76
+ - ๋ชจ๋“  API์— `@login_required` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ ์šฉํ•˜์—ฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
77
+ - ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง์ ‘ LocalPCAgent URL์— ์ ‘๊ทผํ•˜์ง€ ์•Š๊ณ  ํ•ญ์ƒ Flask ๋ฐฑ์—”๋“œ๋ฅผ ํ†ตํ•ด ์ ‘๊ทผ
78
+ - DEVICE_SERVER_URL์€ ์„œ๋ฒ„ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ๋งŒ ๊ด€๋ฆฌ (ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์— ๋…ธ์ถœ ์•ˆ ํ•จ)
79
+
80
+ ## 7. ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•
81
+
82
+ ### ๋กœ์ปฌ ํ…Œ์ŠคํŠธ
83
+ 1. LocalPCAgent ์„œ๋ฒ„ ์‹คํ–‰ ๋ฐ ngrok URL ํ™•์ธ
84
+ 2. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •: `DEVICE_SERVER_URL=<ngrok-url>`
85
+ 3. Flask ์•ฑ ์‹คํ–‰: `python app.py`
86
+ 4. ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ `http://localhost:7860` ์ ‘์†
87
+ 5. ๋กœ๊ทธ์ธ ํ›„ '์žฅ์น˜ ์ œ์–ด' ํƒญ ํ…Œ์ŠคํŠธ
88
+
89
+ ### ๋””๋ฒ„๊น…
90
+ - ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ์ฝ˜์†”: JavaScript ๋กœ๊ทธ ๋ฐ ์˜ค๋ฅ˜ ํ™•์ธ
91
+ - Flask ์„œ๋ฒ„ ๋กœ๊ทธ: API ์š”์ฒญ ๋ฐ ์‘๋‹ต ํ™•์ธ
92
+ - LocalPCAgent ์„œ๋ฒ„ ๋กœ๊ทธ: ์‹ค์ œ ์‹คํ–‰ ๊ฒฐ๊ณผ ํ™•์ธ
93
+
94
+ ## 8. ํ–ฅํ›„ ๊ฐœ์„  ์‚ฌํ•ญ
95
+
96
+ - ์žฅ์น˜ ์ œ์–ด ๋กœ๊ทธ ์ €์žฅ ๋ฐ ์ด๋ ฅ ์กฐํšŒ ๊ธฐ๋Šฅ
97
+ - ์‹คํ–‰ ๊ฒฐ๊ณผ ์Šคํฌ๋ฆฐ์ƒท ์ž๋™ ์บก์ฒ˜ ๋ฐ ํ‘œ์‹œ
98
+ - ์‹คํ–‰ ํ”„๋กœ๊ทธ๋žจ ์Šค์ผ€์ค„๋ง (์˜ˆ์•ฝ ์‹คํ–‰)
99
+ - ๋” ์ƒ์„ธํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜
100
+ - ์žฅ์น˜ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ๊ณผ ์ฑ—๋ด‡์˜ ์—ฐ๋™ (์ฑ„ํŒ…์œผ๋กœ ์žฅ์น˜ ์ œ์–ด ๋ช…๋ น ๋‚ด๋ฆฌ๊ธฐ)