Spaces:
Running
Running
merge1
Browse files- app/static/css/device-style.css +213 -203
- app/static/js/app-core.js +96 -310
- app/static/js/app-device.js +498 -317
- app/static/js/app.js +204 -176
- app/templates/index.html +40 -0
- docs/project_plan.md +100 -0
app/static/css/device-style.css
CHANGED
|
@@ -1,306 +1,316 @@
|
|
| 1 |
-
|
| 2 |
-
* ์ฅ์น ๊ด๋ฆฌ ์ ์ฉ CSS ์คํ์ผ
|
| 3 |
-
*/
|
| 4 |
|
| 5 |
-
/* ์ฅ์น
|
| 6 |
-
|
| 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 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 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 |
-
.
|
| 48 |
-
background-color:
|
| 49 |
-
border-radius:
|
| 50 |
padding: 15px;
|
| 51 |
-
|
| 52 |
-
font-size: 14px;
|
| 53 |
}
|
| 54 |
|
| 55 |
-
.
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
font-
|
| 60 |
}
|
| 61 |
|
| 62 |
-
.
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
margin-bottom:
|
| 66 |
}
|
| 67 |
|
| 68 |
-
.
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
-
|
| 73 |
-
.retry-button {
|
| 74 |
background-color: var(--primary-color);
|
| 75 |
color: white;
|
| 76 |
border: none;
|
| 77 |
border-radius: 4px;
|
| 78 |
-
padding:
|
| 79 |
-
margin-top: 10px;
|
| 80 |
cursor: pointer;
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
justify-content: center;
|
| 84 |
-
font-size: 14px;
|
| 85 |
-
transition: var(--transition);
|
| 86 |
}
|
| 87 |
|
| 88 |
-
.
|
| 89 |
-
background-color: var(--primary-dark);
|
| 90 |
}
|
| 91 |
|
| 92 |
-
.
|
| 93 |
-
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
-
.
|
| 102 |
-
|
| 103 |
-
color:
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
-
.
|
| 110 |
-
|
| 111 |
-
color:
|
| 112 |
-
|
| 113 |
}
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
|
|
|
| 117 |
border-radius: 8px;
|
| 118 |
padding: 15px;
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
transition: var(--transition);
|
| 122 |
}
|
| 123 |
|
| 124 |
-
.device-
|
| 125 |
-
|
| 126 |
}
|
| 127 |
|
| 128 |
-
.device-
|
| 129 |
-
margin-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
-
.
|
| 135 |
-
display:
|
| 136 |
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 137 |
gap: 10px;
|
| 138 |
-
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
-
.
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
-
.
|
| 146 |
-
color: var(--
|
| 147 |
-
font-weight: 500;
|
| 148 |
}
|
| 149 |
|
| 150 |
-
.
|
| 151 |
-
color: var(--
|
| 152 |
-
|
| 153 |
}
|
| 154 |
|
| 155 |
-
.status-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
background-color: #f8f9fa;
|
| 164 |
border-radius: 8px;
|
| 165 |
-
|
| 166 |
-
|
|
|
|
| 167 |
}
|
| 168 |
|
| 169 |
-
.
|
| 170 |
-
|
| 171 |
}
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
justify-content: center;
|
| 179 |
-
padding: 20px;
|
| 180 |
-
text-align: center;
|
| 181 |
}
|
| 182 |
|
| 183 |
-
.
|
| 184 |
-
|
| 185 |
-
height: 20px;
|
| 186 |
-
margin-right: 8px;
|
| 187 |
}
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
padding: 15px;
|
| 194 |
-
margin-top: 20px;
|
| 195 |
-
border: 1px solid var(--border-color);
|
| 196 |
}
|
| 197 |
|
| 198 |
-
.program-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
| 205 |
}
|
| 206 |
|
| 207 |
-
.program-
|
| 208 |
-
|
| 209 |
-
color: var(--primary-color);
|
| 210 |
-
font-size: 16px;
|
| 211 |
}
|
| 212 |
|
| 213 |
-
.program-
|
| 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:
|
| 225 |
cursor: pointer;
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
}
|
| 231 |
|
| 232 |
.execute-btn:hover {
|
| 233 |
-
background-color: var(--primary-dark);
|
| 234 |
}
|
| 235 |
|
| 236 |
-
.execute-btn
|
| 237 |
-
|
|
|
|
| 238 |
}
|
| 239 |
|
| 240 |
-
.execute-
|
| 241 |
-
|
| 242 |
-
align-items: center;
|
| 243 |
-
padding: 5px 10px;
|
| 244 |
border-radius: 4px;
|
| 245 |
margin-top: 10px;
|
| 246 |
-
font-size:
|
| 247 |
}
|
| 248 |
|
| 249 |
-
.execute-
|
| 250 |
-
background-color:
|
| 251 |
-
color:
|
|
|
|
| 252 |
}
|
| 253 |
|
| 254 |
-
.execute-
|
| 255 |
-
background-color: rgba(
|
| 256 |
-
color:
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
-
.execute-
|
| 260 |
-
background-color: rgba(
|
| 261 |
-
color:
|
|
|
|
| 262 |
}
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
}
|
| 267 |
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
display: flex;
|
| 271 |
-
justify-content: space-between;
|
| 272 |
-
margin-bottom: 15px;
|
| 273 |
}
|
| 274 |
|
| 275 |
-
.
|
| 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 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
}
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
}
|
| 295 |
|
| 296 |
-
|
| 297 |
-
|
| 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
|
| 3 |
*/
|
| 4 |
|
| 5 |
-
// ์ ์ญ
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
| 28 |
}
|
| 29 |
-
|
| 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 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 151 |
-
|
| 152 |
-
|
| 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
|
| 196 |
-
|
| 197 |
-
sourcesDiv.appendChild(sourcesTitle);
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 242 |
-
|
| 243 |
-
|
| 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 ๊ฒ์ ์ฑ๋ด ์ฅ์น
|
| 3 |
*/
|
| 4 |
|
| 5 |
-
// ์ฅ์น
|
| 6 |
-
const
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
/**
|
| 13 |
-
* ์ฅ์น ๊ด๋ฆฌ DOM ์์ ์ด๊ธฐํ
|
| 14 |
-
*/
|
| 15 |
-
function initDeviceElements() {
|
| 16 |
-
console.log('์ฅ์น ๊ด๋ฆฌ DOM ์์ ์ด๊ธฐํ ์ค...');
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 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 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 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 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
}
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
//
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
//
|
| 127 |
-
if (
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 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 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
| 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 |
-
|
| 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 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
}
|
| 207 |
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
// ๋ชฉ๋ก ์ด๊ธฐํ
|
| 212 |
-
programsList.innerHTML = '';
|
| 213 |
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
-
data.
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
programDesc.className = 'program-description';
|
| 229 |
-
programDesc.textContent = program.description;
|
| 230 |
-
programItem.appendChild(programDesc);
|
| 231 |
-
}
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 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 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
-
}
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
errorDiv.className = 'error-message';
|
| 257 |
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
} else {
|
| 261 |
-
errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ํ๋ก๊ทธ๋จ ๋ชฉ๋ก ๋ก๋ ์ค๋ฅ: ${error.message}`;
|
| 262 |
-
}
|
| 263 |
|
| 264 |
-
|
|
|
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 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 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 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 |
-
|
|
|
|
| 317 |
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
}
|
| 323 |
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
|
|
|
| 347 |
}
|
| 348 |
|
| 349 |
-
//
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
errorIndicator.innerHTML = `<i class="fas fa-exclamation-circle"></i><span>์คํ ์คํจ</span>`;
|
| 353 |
-
programItem.appendChild(errorIndicator);
|
| 354 |
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
}
|
| 360 |
-
}
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
}
|
| 365 |
-
}
|
| 366 |
|
| 367 |
-
// ํ์ด์ง ๋ก๋ ์ ๋ชจ๋ ์ด๊ธฐํ
|
| 368 |
document.addEventListener('DOMContentLoaded', function() {
|
| 369 |
-
console.log('์ฅ์น
|
| 370 |
|
| 371 |
-
//
|
| 372 |
setTimeout(() => {
|
| 373 |
-
|
| 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('
|
| 48 |
return false;
|
| 49 |
}
|
| 50 |
}
|
|
@@ -55,16 +61,17 @@ async function checkAppStatus() {
|
|
| 55 |
async function loadLLMs() {
|
| 56 |
try {
|
| 57 |
// API ์์ฒญ
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 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 |
-
|
|
|
|
| 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
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
// UI ์
๋ฐ์ดํธ
|
| 253 |
addMessage(message, 'user');
|
|
@@ -259,7 +182,8 @@ async function sendMessage() {
|
|
| 259 |
|
| 260 |
try {
|
| 261 |
// API ์์ฒญ
|
| 262 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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('
|
| 294 |
removeLoadingMessage(loadingMessageId);
|
| 295 |
-
addErrorMessage('์ค๋ฅ๊ฐ
|
| 296 |
}
|
| 297 |
}
|
| 298 |
|
|
@@ -300,9 +223,13 @@ async function sendMessage() {
|
|
| 300 |
* ์์ฑ ๋
น์ ์์ ํจ์
|
| 301 |
*/
|
| 302 |
async function startRecording() {
|
| 303 |
-
if (isRecording)
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
| 314 |
});
|
| 315 |
|
| 316 |
-
mediaRecorder.addEventListener('stop',
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
| 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('
|
| 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 |
-
|
|
|
|
| 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('
|
| 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 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 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('
|
| 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 |
+
- ์ฅ์น ๊ด๋ฆฌ ๊ธฐ๋ฅ๊ณผ ์ฑ๋ด์ ์ฐ๋ (์ฑํ
์ผ๋ก ์ฅ์น ์ ์ด ๋ช
๋ น ๋ด๋ฆฌ๊ธฐ)
|