Spaces:
No application file
No application file
fix
Browse files- app/app_routes.py +176 -191
- app/static/js/app-device.js +148 -160
app/app_routes.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
"""
|
2 |
-
RAG
|
3 |
"""
|
4 |
|
5 |
import os
|
@@ -11,20 +11,19 @@ from flask import request, jsonify, render_template, send_from_directory, sessio
|
|
11 |
from datetime import datetime
|
12 |
from werkzeug.utils import secure_filename
|
13 |
|
14 |
-
# 로거
|
15 |
-
logger = logging.getLogger(__name__)
|
16 |
|
17 |
def register_routes(app, login_required, llm_interface, retriever, stt_client, DocumentProcessor, base_retriever, app_ready, ADMIN_USERNAME, ADMIN_PASSWORD, DEVICE_SERVER_URL):
|
18 |
-
"""Flask
|
19 |
|
20 |
-
#
|
21 |
def allowed_audio_file(filename):
|
22 |
-
"""
|
23 |
ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
|
24 |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
|
25 |
|
26 |
def allowed_doc_file(filename):
|
27 |
-
"""
|
28 |
ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
|
29 |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
|
30 |
|
@@ -32,70 +31,69 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
32 |
def login():
|
33 |
error = None
|
34 |
next_url = request.args.get('next')
|
35 |
-
logger.info(f"--------------
|
36 |
logger.info(f"Method: {request.method}")
|
37 |
|
38 |
if request.method == 'POST':
|
39 |
-
logger.info("
|
40 |
username = request.form.get('username', '')
|
41 |
password = request.form.get('password', '')
|
42 |
-
logger.info(f"
|
43 |
-
logger.info(f"
|
44 |
|
45 |
-
#
|
46 |
valid_username = ADMIN_USERNAME
|
47 |
valid_password = ADMIN_PASSWORD
|
48 |
-
logger.info(f"검증용
|
49 |
-
logger.info(f"검증용
|
50 |
|
51 |
if username == valid_username and password == valid_password:
|
52 |
-
logger.info(f"
|
53 |
-
#
|
54 |
-
logger.debug(f"
|
55 |
|
56 |
-
#
|
57 |
-
session.permanent = True
|
58 |
session['logged_in'] = True
|
59 |
session['username'] = username
|
60 |
session.modified = True
|
61 |
|
62 |
-
logger.info(f"
|
63 |
-
logger.info("
|
64 |
|
65 |
-
#
|
66 |
redirect_to = next_url or url_for('index')
|
67 |
-
logger.info(f"
|
68 |
response = redirect(redirect_to)
|
69 |
return response
|
70 |
else:
|
71 |
-
logger.warning("
|
72 |
-
if username != valid_username: logger.warning("
|
73 |
-
if password != valid_password: logger.warning("
|
74 |
-
error = '
|
75 |
else:
|
76 |
-
logger.info("
|
77 |
if 'logged_in' in session:
|
78 |
-
logger.info("
|
79 |
return redirect(url_for('index'))
|
80 |
|
81 |
-
logger.info("----------
|
82 |
return render_template('login.html', error=error, next=next_url)
|
83 |
|
84 |
|
85 |
@app.route('/logout')
|
86 |
def logout():
|
87 |
-
"""
|
88 |
if 'logged_in' in session:
|
89 |
username = session.get('username', 'unknown')
|
90 |
-
logger.info(f"
|
91 |
session.pop('logged_in', None)
|
92 |
session.pop('username', None)
|
93 |
session.modified = True
|
94 |
-
logger.info(f"
|
95 |
else:
|
96 |
-
logger.warning("
|
97 |
|
98 |
-
logger.info("
|
99 |
response = redirect(url_for('login'))
|
100 |
return response
|
101 |
|
@@ -103,43 +101,42 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
103 |
@app.route('/')
|
104 |
@login_required
|
105 |
def index():
|
106 |
-
"""메인
|
107 |
nonlocal app_ready
|
108 |
|
109 |
-
#
|
110 |
-
current_time = datetime.now()
|
111 |
start_time = datetime.fromtimestamp(os.path.getmtime(__file__))
|
112 |
time_diff = (current_time - start_time).total_seconds()
|
113 |
|
114 |
if not app_ready and time_diff > 30:
|
115 |
-
logger.warning(f"
|
116 |
app_ready = True
|
117 |
|
118 |
if not app_ready:
|
119 |
-
logger.info("
|
120 |
-
return render_template('loading.html'), 503 #
|
121 |
|
122 |
-
logger.info("메인
|
123 |
return render_template('index.html')
|
124 |
|
125 |
|
126 |
@app.route('/api/status')
|
127 |
@login_required
|
128 |
def app_status():
|
129 |
-
"""
|
130 |
-
logger.info(f"
|
131 |
return jsonify({"ready": app_ready})
|
132 |
|
133 |
|
134 |
@app.route('/api/llm', methods=['GET', 'POST'])
|
135 |
@login_required
|
136 |
def llm_api():
|
137 |
-
"""
|
138 |
if not app_ready:
|
139 |
-
return jsonify({"error": "
|
140 |
|
141 |
if request.method == 'GET':
|
142 |
-
logger.info("LLM 목록
|
143 |
try:
|
144 |
current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
|
145 |
supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
|
@@ -152,87 +149,86 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
152 |
"current_llm": current_details
|
153 |
})
|
154 |
except Exception as e:
|
155 |
-
logger.error(f"LLM
|
156 |
-
return jsonify({"error": "LLM
|
157 |
|
158 |
elif request.method == 'POST':
|
159 |
data = request.get_json()
|
160 |
if not data or 'llm_id' not in data:
|
161 |
-
return jsonify({"error": "LLM ID가
|
162 |
|
163 |
llm_id = data['llm_id']
|
164 |
-
logger.info(f"LLM
|
165 |
|
166 |
try:
|
167 |
if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
|
168 |
-
raise NotImplementedError("LLM
|
169 |
|
170 |
if llm_id not in llm_interface.llm_clients:
|
171 |
-
return jsonify({"error": f"
|
172 |
|
173 |
success = llm_interface.set_llm(llm_id)
|
174 |
if success:
|
175 |
new_details = llm_interface.get_current_llm_details()
|
176 |
-
logger.info(f"LLM
|
177 |
return jsonify({
|
178 |
"success": True,
|
179 |
-
"message": f"LLM
|
180 |
"current_llm": new_details
|
181 |
})
|
182 |
else:
|
183 |
-
logger.error(f"LLM
|
184 |
-
return jsonify({"error": "LLM
|
185 |
except Exception as e:
|
186 |
-
logger.error(f"LLM
|
187 |
-
return jsonify({"error": f"LLM
|
188 |
|
189 |
|
190 |
@app.route('/api/chat', methods=['POST'])
|
191 |
@login_required
|
192 |
def chat():
|
193 |
-
"""
|
194 |
if not app_ready or retriever is None:
|
195 |
-
return jsonify({"error": "
|
196 |
|
197 |
try:
|
198 |
data = request.get_json()
|
199 |
if not data or 'query' not in data:
|
200 |
-
return jsonify({"error": "쿼리가
|
201 |
|
202 |
query = data['query']
|
203 |
-
logger.info(f"
|
204 |
|
205 |
-
# RAG
|
206 |
if not hasattr(retriever, 'search'):
|
207 |
-
raise NotImplementedError("Retriever
|
208 |
search_results = retriever.search(query, top_k=5, first_stage_k=6)
|
209 |
|
210 |
-
#
|
211 |
-
|
212 |
-
raise NotImplementedError("DocumentProcessor에 prepare_rag_context 메소드가 없습니다.")
|
213 |
context = DocumentProcessor.prepare_rag_context(search_results, field="text")
|
214 |
|
215 |
if not context:
|
216 |
-
logger.warning("
|
217 |
|
218 |
-
# LLM
|
219 |
llm_id = data.get('llm_id', None)
|
220 |
if not hasattr(llm_interface, 'rag_generate'):
|
221 |
-
raise NotImplementedError("LLMInterface
|
222 |
|
223 |
if not context:
|
224 |
-
answer = "
|
225 |
-
logger.info("
|
226 |
else:
|
227 |
answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
|
228 |
-
logger.info(f"LLM
|
229 |
|
230 |
-
#
|
231 |
sources = []
|
232 |
if search_results:
|
233 |
for result in search_results:
|
234 |
if not isinstance(result, dict):
|
235 |
-
logger.warning(f"
|
236 |
continue
|
237 |
|
238 |
if "source" in result:
|
@@ -241,7 +237,7 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
241 |
"score": result.get("rerank_score", result.get("score", 0))
|
242 |
}
|
243 |
|
244 |
-
# CSV
|
245 |
if "text" in result and result.get("filetype") == "csv":
|
246 |
try:
|
247 |
text_lines = result["text"].strip().split('\n')
|
@@ -250,13 +246,13 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
250 |
if ',' in first_line:
|
251 |
first_column = first_line.split(',')[0].strip()
|
252 |
source_info["id"] = first_column
|
253 |
-
logger.debug(f"CSV
|
254 |
except Exception as e:
|
255 |
-
logger.warning(f"CSV
|
256 |
|
257 |
sources.append(source_info)
|
258 |
|
259 |
-
# 최종
|
260 |
response_data = {
|
261 |
"answer": answer,
|
262 |
"sources": sources,
|
@@ -265,104 +261,100 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
265 |
return jsonify(response_data)
|
266 |
|
267 |
except Exception as e:
|
268 |
-
logger.error(f"채팅 처리
|
269 |
-
return jsonify({"error": f"처리
|
270 |
|
271 |
|
272 |
@app.route('/api/voice', methods=['POST'])
|
273 |
@login_required
|
274 |
def voice_chat():
|
275 |
-
"""
|
276 |
if not app_ready:
|
277 |
-
logger.warning("
|
278 |
-
#
|
279 |
-
#
|
280 |
|
281 |
if retriever is None:
|
282 |
-
logger.error("retriever가
|
283 |
return jsonify({
|
284 |
-
"transcription": "(
|
285 |
-
"answer": "
|
286 |
"sources": []
|
287 |
})
|
288 |
-
#
|
289 |
if stt_client is None:
|
290 |
return jsonify({
|
291 |
-
"transcription": "(
|
292 |
-
"answer": "
|
293 |
"sources": []
|
294 |
})
|
295 |
|
296 |
-
logger.info("
|
297 |
|
298 |
if 'audio' not in request.files:
|
299 |
-
logger.error("
|
300 |
-
return jsonify({"error": "
|
301 |
|
302 |
audio_file = request.files['audio']
|
303 |
-
logger.info(f"
|
304 |
|
305 |
try:
|
306 |
-
#
|
307 |
-
#
|
308 |
with tempfile.NamedTemporaryFile(delete=True) as temp_audio:
|
309 |
audio_file.save(temp_audio.name)
|
310 |
-
logger.info(f"
|
311 |
-
# VitoSTT.transcribe_audio 가
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
# 파일 경로로 전달 시
|
317 |
-
# stt_result = stt_client.transcribe_audio(temp_audio.name, language="ko")
|
318 |
-
# 바이트로 전달 시
|
319 |
-
with open(temp_audio.name, 'rb') as f_bytes:
|
320 |
audio_bytes = f_bytes.read()
|
321 |
stt_result = stt_client.transcribe_audio(audio_bytes, language="ko")
|
322 |
|
323 |
|
324 |
if not isinstance(stt_result, dict) or not stt_result.get("success"):
|
325 |
-
error_msg = stt_result.get("error", "
|
326 |
-
logger.error(f"
|
327 |
return jsonify({
|
328 |
-
"error": "
|
329 |
"details": error_msg
|
330 |
}), 500
|
331 |
|
332 |
transcription = stt_result.get("text", "")
|
333 |
if not transcription:
|
334 |
-
logger.warning("
|
335 |
-
return jsonify({"error": "
|
336 |
|
337 |
-
logger.info(f"
|
338 |
if retriever is None:
|
339 |
-
logger.error("STT
|
340 |
return jsonify({
|
341 |
"transcription": transcription,
|
342 |
-
"answer": "
|
343 |
"sources": []
|
344 |
})
|
345 |
-
# ---
|
346 |
-
# RAG
|
347 |
search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
|
348 |
context = DocumentProcessor.prepare_rag_context(search_results, field="text")
|
349 |
|
350 |
if not context:
|
351 |
-
logger.warning("
|
352 |
-
# answer = "
|
353 |
pass
|
354 |
|
355 |
-
# LLM
|
356 |
-
llm_id = request.form.get('llm_id', None) #
|
357 |
if not context:
|
358 |
-
answer = "
|
359 |
-
logger.info("
|
360 |
else:
|
361 |
answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
|
362 |
-
logger.info(f"LLM
|
363 |
|
364 |
|
365 |
-
#
|
366 |
enhanced_sources = []
|
367 |
if search_results:
|
368 |
for doc in search_results:
|
@@ -381,10 +373,10 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
381 |
first_column = first_line.split(',')[0].strip()
|
382 |
source_info["id"] = first_column
|
383 |
except Exception as e:
|
384 |
-
logger.warning(f"[
|
385 |
enhanced_sources.append(source_info)
|
386 |
|
387 |
-
# 최종
|
388 |
response_data = {
|
389 |
"transcription": transcription,
|
390 |
"answer": answer,
|
@@ -394,9 +386,9 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
394 |
return jsonify(response_data)
|
395 |
|
396 |
except Exception as e:
|
397 |
-
logger.error(f"
|
398 |
return jsonify({
|
399 |
-
"error": "
|
400 |
"details": str(e)
|
401 |
}), 500
|
402 |
|
@@ -404,45 +396,45 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
404 |
@app.route('/api/upload', methods=['POST'])
|
405 |
@login_required
|
406 |
def upload_document():
|
407 |
-
"""
|
408 |
-
if
|
409 |
-
return jsonify({"error": "
|
410 |
|
411 |
if 'document' not in request.files:
|
412 |
-
return jsonify({"error": "문서
|
413 |
|
414 |
doc_file = request.files['document']
|
415 |
if doc_file.filename == '':
|
416 |
-
return jsonify({"error": "
|
417 |
|
418 |
if not allowed_doc_file(doc_file.filename):
|
419 |
-
logger.error(f"
|
420 |
-
return jsonify({"error": f"
|
421 |
|
422 |
try:
|
423 |
filename = secure_filename(doc_file.filename)
|
424 |
filepath = os.path.join(app.config['DATA_FOLDER'], filename)
|
425 |
doc_file.save(filepath)
|
426 |
-
logger.info(f"문서
|
427 |
|
428 |
-
# 문서 처리 (
|
429 |
try:
|
430 |
with open(filepath, 'r', encoding='utf-8') as f:
|
431 |
content = f.read()
|
432 |
except UnicodeDecodeError:
|
433 |
-
logger.info(f"UTF-8
|
434 |
try:
|
435 |
with open(filepath, 'r', encoding='cp949') as f:
|
436 |
content = f.read()
|
437 |
except Exception as e_cp949:
|
438 |
-
logger.error(f"CP949
|
439 |
-
return jsonify({"error": "
|
440 |
except Exception as e_read:
|
441 |
-
logger.error(f"
|
442 |
-
return jsonify({"error": f"
|
443 |
|
444 |
|
445 |
-
#
|
446 |
metadata = {
|
447 |
"source": filename, "filename": filename,
|
448 |
"filetype": filename.rsplit('.', 1)[1].lower(),
|
@@ -452,91 +444,84 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
452 |
docs = []
|
453 |
|
454 |
if not hasattr(DocumentProcessor, 'csv_to_documents') or not hasattr(DocumentProcessor, 'text_to_documents'):
|
455 |
-
raise NotImplementedError("DocumentProcessor
|
456 |
|
457 |
if file_ext == 'csv':
|
458 |
-
logger.info(f"CSV
|
459 |
-
docs = DocumentProcessor.csv_to_documents(content, metadata) #
|
460 |
-
|
461 |
-
|
462 |
-
# PDF, DOCX 등은 별도 라이브러리(pypdf, python-docx) 필요
|
463 |
if file_ext in ['pdf', 'docx']:
|
464 |
-
logger.warning(f".{file_ext}
|
465 |
-
#
|
466 |
-
#
|
467 |
# content = extract_text_from_docx(filepath)
|
468 |
-
#
|
469 |
-
content = ""
|
470 |
|
471 |
-
if content: #
|
472 |
docs = DocumentProcessor.text_to_documents(
|
473 |
content, metadata=metadata,
|
474 |
chunk_size=512, chunk_overlap=50
|
475 |
)
|
476 |
|
477 |
-
#
|
478 |
-
if docs:
|
479 |
if not hasattr(base_retriever, 'add_documents') or not hasattr(base_retriever, 'save'):
|
480 |
-
raise NotImplementedError("기본
|
481 |
|
482 |
-
logger.info(f"{len(docs)}
|
483 |
base_retriever.add_documents(docs)
|
484 |
|
485 |
-
#
|
486 |
-
logger.info(f"
|
487 |
index_path = app.config['INDEX_PATH']
|
488 |
try:
|
489 |
base_retriever.save(index_path)
|
490 |
-
logger.info("
|
491 |
-
#
|
492 |
-
#
|
493 |
return jsonify({
|
494 |
"success": True,
|
495 |
-
"message": f"
|
496 |
})
|
497 |
except Exception as e_save:
|
498 |
-
logger.error(f"
|
499 |
-
return jsonify({"error": f"
|
500 |
else:
|
501 |
-
logger.warning(f"
|
502 |
-
#
|
503 |
return jsonify({
|
504 |
"warning": True,
|
505 |
-
"message": f"
|
506 |
})
|
507 |
|
508 |
except Exception as e:
|
509 |
-
logger.error(f"
|
510 |
-
return jsonify({"error": f"
|
511 |
|
512 |
|
513 |
@app.route('/api/documents', methods=['GET'])
|
514 |
@login_required
|
515 |
def list_documents():
|
516 |
-
"""
|
517 |
-
if
|
518 |
-
return jsonify({"error": "
|
519 |
|
520 |
try:
|
521 |
sources = {}
|
522 |
total_chunks = 0
|
523 |
-
# base_retriever.documents
|
524 |
-
|
525 |
-
logger.info(f"총 {len(base_retriever.documents)}개 문서 청크에서 소스 목록 생성 중...")
|
526 |
for doc in base_retriever.documents:
|
527 |
-
# 문서
|
528 |
-
if not isinstance(doc, dict): continue
|
529 |
|
530 |
-
source = doc.get("source", "unknown") #
|
531 |
-
if source == "unknown" and "metadata" in doc and isinstance(doc["metadata"], dict):
|
532 |
source = doc["metadata"].get("source", "unknown") # Langchain Document 구조 고려
|
533 |
|
534 |
if source != "unknown":
|
535 |
if source in sources:
|
536 |
sources[source]["chunks"] += 1
|
537 |
else:
|
538 |
-
#
|
539 |
-
filename = doc.get("filename", source)
|
540 |
filetype = doc.get("filetype", "unknown")
|
541 |
if "metadata" in doc and isinstance(doc["metadata"], dict):
|
542 |
filename = doc["metadata"].get("filename", filename)
|
@@ -549,13 +534,13 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
549 |
}
|
550 |
total_chunks += 1
|
551 |
else:
|
552 |
-
logger.info("
|
553 |
|
554 |
-
# 목록
|
555 |
documents = [{"source": src, **info} for src, info in sources.items()]
|
556 |
documents.sort(key=lambda x: x["chunks"], reverse=True)
|
557 |
|
558 |
-
logger.info(f"문서 목록 조회
|
559 |
return jsonify({
|
560 |
"documents": documents,
|
561 |
"total_documents": len(documents),
|
@@ -563,5 +548,5 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
|
|
563 |
})
|
564 |
|
565 |
except Exception as e:
|
566 |
-
logger.error(f"문서 목록 조회
|
567 |
-
return jsonify({"error": f"문서 목록 조회
|
|
|
1 |
"""
|
2 |
+
RAG 검??챗봇 ???�플리�??�션 - API ?�우???�의
|
3 |
"""
|
4 |
|
5 |
import os
|
|
|
11 |
from datetime import datetime
|
12 |
from werkzeug.utils import secure_filename
|
13 |
|
14 |
+
# 로거 가?�오�?logger = logging.getLogger(__name__)
|
|
|
15 |
|
16 |
def register_routes(app, login_required, llm_interface, retriever, stt_client, DocumentProcessor, base_retriever, app_ready, ADMIN_USERNAME, ADMIN_PASSWORD, DEVICE_SERVER_URL):
|
17 |
+
"""Flask ?�플리�??�션??기본 ?�우???�록"""
|
18 |
|
19 |
+
# ?�퍼 ?�수
|
20 |
def allowed_audio_file(filename):
|
21 |
+
"""?�일???�용???�디???�장?��? 가지?��? ?�인"""
|
22 |
ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
|
23 |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
|
24 |
|
25 |
def allowed_doc_file(filename):
|
26 |
+
"""?�일???�용??문서 ?�장?��? 가지?��? ?�인"""
|
27 |
ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
|
28 |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
|
29 |
|
|
|
31 |
def login():
|
32 |
error = None
|
33 |
next_url = request.args.get('next')
|
34 |
+
logger.info(f"-------------- 로그???�이지 ?�속 (Next: {next_url}) --------------")
|
35 |
logger.info(f"Method: {request.method}")
|
36 |
|
37 |
if request.method == 'POST':
|
38 |
+
logger.info("로그???�도 받음")
|
39 |
username = request.form.get('username', '')
|
40 |
password = request.form.get('password', '')
|
41 |
+
logger.info(f"?�력???�용?�명: {username}")
|
42 |
+
logger.info(f"비�?번호 ?�력 ?��?: {len(password) > 0}")
|
43 |
|
44 |
+
# ?�경 변???�는 기본값과 비교
|
45 |
valid_username = ADMIN_USERNAME
|
46 |
valid_password = ADMIN_PASSWORD
|
47 |
+
logger.info(f"검증용 ?�용?�명: {valid_username}")
|
48 |
+
logger.info(f"검증용 비�?번호 존재 ?��?: {valid_password is not None and len(valid_password) > 0}")
|
49 |
|
50 |
if username == valid_username and password == valid_password:
|
51 |
+
logger.info(f"로그???�공: {username}")
|
52 |
+
# ?�션 ?�정 ???�재 ?�션 ?�태 로깅
|
53 |
+
logger.debug(f"?�션 ?�정 ?? {session}")
|
54 |
|
55 |
+
# ?�션??로그???�보 ?�?? session.permanent = True
|
|
|
56 |
session['logged_in'] = True
|
57 |
session['username'] = username
|
58 |
session.modified = True
|
59 |
|
60 |
+
logger.info(f"?�션 ?�정 ?? {session}")
|
61 |
+
logger.info("?�션 ?�정 ?�료, 리디?�션 ?�도")
|
62 |
|
63 |
+
# 로그???�공 ??리디?�션
|
64 |
redirect_to = next_url or url_for('index')
|
65 |
+
logger.info(f"리디?�션 ?�?? {redirect_to}")
|
66 |
response = redirect(redirect_to)
|
67 |
return response
|
68 |
else:
|
69 |
+
logger.warning("로그???�패: ?�이???�는 비�?번호 불일�?)
|
70 |
+
if username != valid_username: logger.warning("?�용?�명 불일�?)
|
71 |
+
if password != valid_password: logger.warning("비�?번호 불일�?)
|
72 |
+
error = '?�이???�는 비�?번호�� ?�바르�? ?�습?�다.'
|
73 |
else:
|
74 |
+
logger.info("로그???�이지 GET ?�청")
|
75 |
if 'logged_in' in session:
|
76 |
+
logger.info("?��? 로그?�된 ?�용?? 메인 ?�이지�?리디?�션")
|
77 |
return redirect(url_for('index'))
|
78 |
|
79 |
+
logger.info("---------- 로그???�이지 ?�더�?----------")
|
80 |
return render_template('login.html', error=error, next=next_url)
|
81 |
|
82 |
|
83 |
@app.route('/logout')
|
84 |
def logout():
|
85 |
+
"""로그?�웃 처리"""
|
86 |
if 'logged_in' in session:
|
87 |
username = session.get('username', 'unknown')
|
88 |
+
logger.info(f"?�용??{username} 로그?�웃 처리 ?�작")
|
89 |
session.pop('logged_in', None)
|
90 |
session.pop('username', None)
|
91 |
session.modified = True
|
92 |
+
logger.info(f"?�션 ?�보 ??�� ?�료. ?�재 ?�션: {session}")
|
93 |
else:
|
94 |
+
logger.warning("로그?�되지 ?��? ?�태?�서 로그?�웃 ?�도")
|
95 |
|
96 |
+
logger.info("로그???�이지�?리디?�션")
|
97 |
response = redirect(url_for('login'))
|
98 |
return response
|
99 |
|
|
|
101 |
@app.route('/')
|
102 |
@login_required
|
103 |
def index():
|
104 |
+
"""메인 ?�이지"""
|
105 |
nonlocal app_ready
|
106 |
|
107 |
+
# ??준�??�태 ?�인 - 30�??�상 지?�으�?강제�?ready ?�태�?변�? current_time = datetime.now()
|
|
|
108 |
start_time = datetime.fromtimestamp(os.path.getmtime(__file__))
|
109 |
time_diff = (current_time - start_time).total_seconds()
|
110 |
|
111 |
if not app_ready and time_diff > 30:
|
112 |
+
logger.warning(f"?�이 30�??�상 초기??�??�태?�니?? 강제�?ready ?�태�?변경합?�다.")
|
113 |
app_ready = True
|
114 |
|
115 |
if not app_ready:
|
116 |
+
logger.info("?�이 ?�직 준비되지 ?�아 로딩 ?�이지 ?�시")
|
117 |
+
return render_template('loading.html'), 503 # ?�비??준�??�됨 ?�태 코드
|
118 |
|
119 |
+
logger.info("메인 ?�이지 ?�청")
|
120 |
return render_template('index.html')
|
121 |
|
122 |
|
123 |
@app.route('/api/status')
|
124 |
@login_required
|
125 |
def app_status():
|
126 |
+
"""??초기???�태 ?�인 API"""
|
127 |
+
logger.info(f"???�태 ?�인 ?�청: {'Ready' if app_ready else 'Not Ready'}")
|
128 |
return jsonify({"ready": app_ready})
|
129 |
|
130 |
|
131 |
@app.route('/api/llm', methods=['GET', 'POST'])
|
132 |
@login_required
|
133 |
def llm_api():
|
134 |
+
"""?�용 가?�한 LLM 목록 �??�택 API"""
|
135 |
if not app_ready:
|
136 |
+
return jsonify({"error": "?�이 ?�직 초기??중입?�다. ?�시 ???�시 ?�도?�주?�요."}), 503
|
137 |
|
138 |
if request.method == 'GET':
|
139 |
+
logger.info("LLM 목록 ?�청")
|
140 |
try:
|
141 |
current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
|
142 |
supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
|
|
|
149 |
"current_llm": current_details
|
150 |
})
|
151 |
except Exception as e:
|
152 |
+
logger.error(f"LLM ?�보 조회 ?�류: {e}")
|
153 |
+
return jsonify({"error": "LLM ?�보 조회 �??�류 발생"}), 500
|
154 |
|
155 |
elif request.method == 'POST':
|
156 |
data = request.get_json()
|
157 |
if not data or 'llm_id' not in data:
|
158 |
+
return jsonify({"error": "LLM ID가 ?�공?��? ?�았?�니??"}), 400
|
159 |
|
160 |
llm_id = data['llm_id']
|
161 |
+
logger.info(f"LLM 변�??�청: {llm_id}")
|
162 |
|
163 |
try:
|
164 |
if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
|
165 |
+
raise NotImplementedError("LLM ?�터?�이?�에 ?�요??메소???�성 ?�음")
|
166 |
|
167 |
if llm_id not in llm_interface.llm_clients:
|
168 |
+
return jsonify({"error": f"지?�되지 ?�는 LLM ID: {llm_id}"}), 400
|
169 |
|
170 |
success = llm_interface.set_llm(llm_id)
|
171 |
if success:
|
172 |
new_details = llm_interface.get_current_llm_details()
|
173 |
+
logger.info(f"LLM??'{new_details.get('name', llm_id)}'�?변경되?�습?�다.")
|
174 |
return jsonify({
|
175 |
"success": True,
|
176 |
+
"message": f"LLM??'{new_details.get('name', llm_id)}'�?변경되?�습?�다.",
|
177 |
"current_llm": new_details
|
178 |
})
|
179 |
else:
|
180 |
+
logger.error(f"LLM 변�??�패 (ID: {llm_id})")
|
181 |
+
return jsonify({"error": "LLM 변�?�??��? ?�류 발생"}), 500
|
182 |
except Exception as e:
|
183 |
+
logger.error(f"LLM 변�?처리 �??�류: {e}", exc_info=True)
|
184 |
+
return jsonify({"error": f"LLM 변�?�??�류 발생: {str(e)}"}), 500
|
185 |
|
186 |
|
187 |
@app.route('/api/chat', methods=['POST'])
|
188 |
@login_required
|
189 |
def chat():
|
190 |
+
"""?�스??기반 챗봇 API"""
|
191 |
if not app_ready or retriever is None:
|
192 |
+
return jsonify({"error": "??검?�기가 ?�직 초기??중입?�다. ?�시 ???�시 ?�도?�주?�요."}), 503
|
193 |
|
194 |
try:
|
195 |
data = request.get_json()
|
196 |
if not data or 'query' not in data:
|
197 |
+
return jsonify({"error": "쿼리가 ?�공?��? ?�았?�니??"}), 400
|
198 |
|
199 |
query = data['query']
|
200 |
+
logger.info(f"?�스??쿼리 ?�신: {query[:100]}...")
|
201 |
|
202 |
+
# RAG 검???�행
|
203 |
if not hasattr(retriever, 'search'):
|
204 |
+
raise NotImplementedError("Retriever??search 메소?��? ?�습?�다.")
|
205 |
search_results = retriever.search(query, top_k=5, first_stage_k=6)
|
206 |
|
207 |
+
# 컨텍?�트 준�? if not hasattr(DocumentProcessor, 'prepare_rag_context'):
|
208 |
+
raise NotImplementedError("DocumentProcessor??prepare_rag_context 메소?��? ?�습?�다.")
|
|
|
209 |
context = DocumentProcessor.prepare_rag_context(search_results, field="text")
|
210 |
|
211 |
if not context:
|
212 |
+
logger.warning("검??결과가 ?�어 컨텍?�트�??�성?��? 못함.")
|
213 |
|
214 |
+
# LLM??질의
|
215 |
llm_id = data.get('llm_id', None)
|
216 |
if not hasattr(llm_interface, 'rag_generate'):
|
217 |
+
raise NotImplementedError("LLMInterface??rag_generate 메소?��? ?�습?�다.")
|
218 |
|
219 |
if not context:
|
220 |
+
answer = "죄송?�니?? 관???�보�?찾을 ???�습?�다."
|
221 |
+
logger.info("컨텍?�트 ?�이 기본 ?�답 ?�성")
|
222 |
else:
|
223 |
answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
|
224 |
+
logger.info(f"LLM ?�답 ?�성 ?�료 (길이: {len(answer)})")
|
225 |
|
226 |
+
# ?�스 ?�보 추출 (CSV ID 추출 로직 ?�함)
|
227 |
sources = []
|
228 |
if search_results:
|
229 |
for result in search_results:
|
230 |
if not isinstance(result, dict):
|
231 |
+
logger.warning(f"?�상�?못한 검??결과 ?�식: {type(result)}")
|
232 |
continue
|
233 |
|
234 |
if "source" in result:
|
|
|
237 |
"score": result.get("rerank_score", result.get("score", 0))
|
238 |
}
|
239 |
|
240 |
+
# CSV ?�일 ?�정 처리
|
241 |
if "text" in result and result.get("filetype") == "csv":
|
242 |
try:
|
243 |
text_lines = result["text"].strip().split('\n')
|
|
|
246 |
if ',' in first_line:
|
247 |
first_column = first_line.split(',')[0].strip()
|
248 |
source_info["id"] = first_column
|
249 |
+
logger.debug(f"CSV ?�스 ID 추출: {first_column} from {source_info['source']}")
|
250 |
except Exception as e:
|
251 |
+
logger.warning(f"CSV ?�스 ID 추출 ?�패 ({result.get('source')}): {e}")
|
252 |
|
253 |
sources.append(source_info)
|
254 |
|
255 |
+
# 최종 ?�답
|
256 |
response_data = {
|
257 |
"answer": answer,
|
258 |
"sources": sources,
|
|
|
261 |
return jsonify(response_data)
|
262 |
|
263 |
except Exception as e:
|
264 |
+
logger.error(f"채팅 처리 �??�류 발생: {e}", exc_info=True)
|
265 |
+
return jsonify({"error": f"처리 �??�류가 발생?�습?�다: {str(e)}"}), 500
|
266 |
|
267 |
|
268 |
@app.route('/api/voice', methods=['POST'])
|
269 |
@login_required
|
270 |
def voice_chat():
|
271 |
+
"""?�성 �?API ?�드?�인??""
|
272 |
if not app_ready:
|
273 |
+
logger.warning("??초기?��? ?�료?��? ?�았지�??�성 API ?�청 처리 ?�도")
|
274 |
+
# ?�기??바로 리턴?��? ?�고 계속 진행
|
275 |
+
# ?�전 검?? retriever?� stt_client가 ?��?�?초기?�되?�는지 ?�인
|
276 |
|
277 |
if retriever is None:
|
278 |
+
logger.error("retriever가 ?�직 초기?�되지 ?�았?�니??)
|
279 |
return jsonify({
|
280 |
+
"transcription": "(?�성???�스?�로 변?�했지�?검???�진???�직 준비되지 ?�았?�니??",
|
281 |
+
"answer": "죄송?�니?? 검???�진???�직 초기??중입?�다. ?�시 ???�시 ?�도?�주?�요.",
|
282 |
"sources": []
|
283 |
})
|
284 |
+
# ?�는 ?�수 컴포?�트가 ?�을 ?�만 ?�별 ?�답 반환
|
285 |
if stt_client is None:
|
286 |
return jsonify({
|
287 |
+
"transcription": "(?�성 ?�식 기능??준�?중입?�다)",
|
288 |
+
"answer": "죄송?�니?? ?�재 ?�성 ?�식 ?�비?��? 초기??중입?�다. ?�시 ???�시 ?�도?�주?�요.",
|
289 |
"sources": []
|
290 |
})
|
291 |
|
292 |
+
logger.info("?�성 �??�청 ?�신")
|
293 |
|
294 |
if 'audio' not in request.files:
|
295 |
+
logger.error("?�디???�일???�공?��? ?�음")
|
296 |
+
return jsonify({"error": "?�디???�일???�공?��? ?�았?�니??"}), 400
|
297 |
|
298 |
audio_file = request.files['audio']
|
299 |
+
logger.info(f"?�신???�디???�일: {audio_file.filename} ({audio_file.content_type})")
|
300 |
|
301 |
try:
|
302 |
+
# ?�디???�일 처리
|
303 |
+
# ?�시 ?�일 ?�용 고려 (메모�?부??줄이�??�해)
|
304 |
with tempfile.NamedTemporaryFile(delete=True) as temp_audio:
|
305 |
audio_file.save(temp_audio.name)
|
306 |
+
logger.info(f"?�디???�일???�시 ?�?? {temp_audio.name}")
|
307 |
+
# VitoSTT.transcribe_audio 가 ?�일 경로 ?�는 바이?��? 받을 ???�도�?구현?�어???? # ?�기?�는 ?�일 경로�??�용?�다�?가?? if not hasattr(stt_client, 'transcribe_audio'):
|
308 |
+
raise NotImplementedError("STT ?�라?�언?�에 transcribe_audio 메소?��? ?�습?�다.")
|
309 |
+
|
310 |
+
# ?�일 경로�??�달 ?? # stt_result = stt_client.transcribe_audio(temp_audio.name, language="ko")
|
311 |
+
# 바이?�로 ?�달 ?? with open(temp_audio.name, 'rb') as f_bytes:
|
|
|
|
|
|
|
|
|
312 |
audio_bytes = f_bytes.read()
|
313 |
stt_result = stt_client.transcribe_audio(audio_bytes, language="ko")
|
314 |
|
315 |
|
316 |
if not isinstance(stt_result, dict) or not stt_result.get("success"):
|
317 |
+
error_msg = stt_result.get("error", "?????�는 STT ?�류") if isinstance(stt_result, dict) else "STT 결과 ?�식 ?�류"
|
318 |
+
logger.error(f"?�성?�식 ?�패: {error_msg}")
|
319 |
return jsonify({
|
320 |
+
"error": "?�성?�식 ?�패",
|
321 |
"details": error_msg
|
322 |
}), 500
|
323 |
|
324 |
transcription = stt_result.get("text", "")
|
325 |
if not transcription:
|
326 |
+
logger.warning("?�성?�식 결과가 비어?�습?�다.")
|
327 |
+
return jsonify({"error": "?�성?�서 ?�스?��? ?�식?��? 못했?�니??", "transcription": ""}), 400
|
328 |
|
329 |
+
logger.info(f"?�성?�식 ?�공: {transcription[:50]}...")
|
330 |
if retriever is None:
|
331 |
+
logger.error("STT ?�공 ??검???�도 �?retriever가 None??)
|
332 |
return jsonify({
|
333 |
"transcription": transcription,
|
334 |
+
"answer": "?�성???�식?��?�? ?�재 검???�스?�이 준비되지 ?�았?�니?? ?�시 ???�시 ?�도?�주?�요.",
|
335 |
"sources": []
|
336 |
})
|
337 |
+
# --- ?�후 로직?� /api/chat�?거의 ?�일 ---
|
338 |
+
# RAG 검???�행
|
339 |
search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
|
340 |
context = DocumentProcessor.prepare_rag_context(search_results, field="text")
|
341 |
|
342 |
if not context:
|
343 |
+
logger.warning("?�성 쿼리???�??검??결과 ?�음.")
|
344 |
+
# answer = "죄송?�니?? 관???�보�?찾을 ???�습?�다." (?�래 LLM ?�출 로직?�서 처리)
|
345 |
pass
|
346 |
|
347 |
+
# LLM ?�출
|
348 |
+
llm_id = request.form.get('llm_id', None) # ?�성 ?�청?� form ?�이?�로 LLM ID 받을 ???�음
|
349 |
if not context:
|
350 |
+
answer = "죄송?�니?? 관???�보�?찾을 ???�습?�다."
|
351 |
+
logger.info("컨텍?�트 ?�이 기본 ?�답 ?�성")
|
352 |
else:
|
353 |
answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
|
354 |
+
logger.info(f"LLM ?�답 ?�성 ?�료 (길이: {len(answer)})")
|
355 |
|
356 |
|
357 |
+
# ?�스 ?�보 추출
|
358 |
enhanced_sources = []
|
359 |
if search_results:
|
360 |
for doc in search_results:
|
|
|
373 |
first_column = first_line.split(',')[0].strip()
|
374 |
source_info["id"] = first_column
|
375 |
except Exception as e:
|
376 |
+
logger.warning(f"[?�성�? CSV ?�스 ID 추출 ?�패 ({doc.get('source')}): {e}")
|
377 |
enhanced_sources.append(source_info)
|
378 |
|
379 |
+
# 최종 ?�답
|
380 |
response_data = {
|
381 |
"transcription": transcription,
|
382 |
"answer": answer,
|
|
|
386 |
return jsonify(response_data)
|
387 |
|
388 |
except Exception as e:
|
389 |
+
logger.error(f"?�성 �?처리 �??�류 발생: {e}", exc_info=True)
|
390 |
return jsonify({
|
391 |
+
"error": "?�성 처리 �??��? ?�류 발생",
|
392 |
"details": str(e)
|
393 |
}), 500
|
394 |
|
|
|
396 |
@app.route('/api/upload', methods=['POST'])
|
397 |
@login_required
|
398 |
def upload_document():
|
399 |
+
"""지?�베?�스 문서 ?�로??API"""
|
400 |
+
if base_retriever is None:
|
401 |
+
return jsonify({"error": "??기본 검?�기가 ?�직 초기??중입?�다."}), 503
|
402 |
|
403 |
if 'document' not in request.files:
|
404 |
+
return jsonify({"error": "문서 ?�일???�공?��? ?�았?�니??"}), 400
|
405 |
|
406 |
doc_file = request.files['document']
|
407 |
if doc_file.filename == '':
|
408 |
+
return jsonify({"error": "?�택???�일???�습?�다."}), 400
|
409 |
|
410 |
if not allowed_doc_file(doc_file.filename):
|
411 |
+
logger.error(f"?�용?��? ?�는 ?�일 ?�식: {doc_file.filename}")
|
412 |
+
return jsonify({"error": f"?�용?��? ?�는 ?�일 ?�식?�니?? ?�용: {', '.join(ALLOWED_DOC_EXTENSIONS)}"}), 400
|
413 |
|
414 |
try:
|
415 |
filename = secure_filename(doc_file.filename)
|
416 |
filepath = os.path.join(app.config['DATA_FOLDER'], filename)
|
417 |
doc_file.save(filepath)
|
418 |
+
logger.info(f"문서 ?�???�료: {filepath}")
|
419 |
|
420 |
+
# 문서 처리 (?�코??처리 ?�함)
|
421 |
try:
|
422 |
with open(filepath, 'r', encoding='utf-8') as f:
|
423 |
content = f.read()
|
424 |
except UnicodeDecodeError:
|
425 |
+
logger.info(f"UTF-8 ?�코???�패, CP949�??�도: {filename}")
|
426 |
try:
|
427 |
with open(filepath, 'r', encoding='cp949') as f:
|
428 |
content = f.read()
|
429 |
except Exception as e_cp949:
|
430 |
+
logger.error(f"CP949 ?�코???�패 ({filename}): {e_cp949}")
|
431 |
+
return jsonify({"error": "?�일 ?�코?�을 ?�을 ???�습?�다 (UTF-8, CP949 ?�도 ?�패)."}), 400
|
432 |
except Exception as e_read:
|
433 |
+
logger.error(f"?�일 ?�기 ?�류 ({filename}): {e_read}")
|
434 |
+
return jsonify({"error": f"?�일 ?�기 �??�류 발생: {str(e_read)}"}), 500
|
435 |
|
436 |
|
437 |
+
# 메�??�이??�?문서 분할/처리
|
438 |
metadata = {
|
439 |
"source": filename, "filename": filename,
|
440 |
"filetype": filename.rsplit('.', 1)[1].lower(),
|
|
|
444 |
docs = []
|
445 |
|
446 |
if not hasattr(DocumentProcessor, 'csv_to_documents') or not hasattr(DocumentProcessor, 'text_to_documents'):
|
447 |
+
raise NotImplementedError("DocumentProcessor???�요??메소???�음")
|
448 |
|
449 |
if file_ext == 'csv':
|
450 |
+
logger.info(f"CSV ?�일 처리 ?�작: {filename}")
|
451 |
+
docs = DocumentProcessor.csv_to_documents(content, metadata) # ???�위 처리 가?? else: # 기�? ?�스??기반 문서
|
452 |
+
logger.info(f"?�반 ?�스??문서 처리 ?�작: {filename}")
|
453 |
+
# PDF, DOCX ?��? 별도 ?�이브러�?pypdf, python-docx) ?�요
|
|
|
454 |
if file_ext in ['pdf', 'docx']:
|
455 |
+
logger.warning(f".{file_ext} ?�일 처리???�재 구현?��? ?�았?�니?? ?�스??추출 로직 추�? ?�요.")
|
456 |
+
# ?�기??pdf/docx ?�스??추출 로직 추�?
|
457 |
+
# ?? content = extract_text_from_pdf(filepath)
|
458 |
# content = extract_text_from_docx(filepath)
|
459 |
+
# ?�시�?비워?? content = ""
|
|
|
460 |
|
461 |
+
if content: # ?�스???�용???�을 ?�만 처리
|
462 |
docs = DocumentProcessor.text_to_documents(
|
463 |
content, metadata=metadata,
|
464 |
chunk_size=512, chunk_overlap=50
|
465 |
)
|
466 |
|
467 |
+
# 검?�기??문서 추�? �??�덱???�?? if docs:
|
|
|
468 |
if not hasattr(base_retriever, 'add_documents') or not hasattr(base_retriever, 'save'):
|
469 |
+
raise NotImplementedError("기본 검?�기??add_documents ?�는 save 메소???�음")
|
470 |
|
471 |
+
logger.info(f"{len(docs)}�?문서 �?���?검?�기??추�??�니??..")
|
472 |
base_retriever.add_documents(docs)
|
473 |
|
474 |
+
# ?�덱???�??(?�로?�마???�??- 비효?�적?????�음)
|
475 |
+
logger.info(f"검?�기 ?�태�??�?�합?�다...")
|
476 |
index_path = app.config['INDEX_PATH']
|
477 |
try:
|
478 |
base_retriever.save(index_path)
|
479 |
+
logger.info("?�덱???�???�료")
|
480 |
+
# ?�순?�화 검?�기???�데?�트 ?�요 ??로직 추�?
|
481 |
+
# ?? retriever.update_base_retriever(base_retriever)
|
482 |
return jsonify({
|
483 |
"success": True,
|
484 |
+
"message": f"?�일 '{filename}' ?�로??�?처리 ?�료 ({len(docs)}�?�?�� 추�?)."
|
485 |
})
|
486 |
except Exception as e_save:
|
487 |
+
logger.error(f"?�덱???�??�??�류 발생: {e_save}")
|
488 |
+
return jsonify({"error": f"?�덱???�??�??�류: {str(e_save)}"}), 500
|
489 |
else:
|
490 |
+
logger.warning(f"?�일 '{filename}'?�서 처리???�용???�거??지?�되지 ?�는 ?�식?�니??")
|
491 |
+
# ?�일?� ?�?�되?�으므�??�공?�로 간주?��? 결정 ?�요
|
492 |
return jsonify({
|
493 |
"warning": True,
|
494 |
+
"message": f"?�일 '{filename}'???�?�되?��?�?처리???�용???�습?�다."
|
495 |
})
|
496 |
|
497 |
except Exception as e:
|
498 |
+
logger.error(f"?�일 ?�로???�는 처리 �??�류 발생: {e}", exc_info=True)
|
499 |
+
return jsonify({"error": f"?�일 ?�로??�??�류: {str(e)}"}), 500
|
500 |
|
501 |
|
502 |
@app.route('/api/documents', methods=['GET'])
|
503 |
@login_required
|
504 |
def list_documents():
|
505 |
+
"""지?�베?�스 문서 목록 API"""
|
506 |
+
if base_retriever is None:
|
507 |
+
return jsonify({"error": "??기본 검?�기가 ?�직 초기??중입?�다."}), 503
|
508 |
|
509 |
try:
|
510 |
sources = {}
|
511 |
total_chunks = 0
|
512 |
+
# base_retriever.documents ?� 같�? ?�성???�제 ?�래?�에 ?�다�?가?? if hasattr(base_retriever, 'documents') and base_retriever.documents:
|
513 |
+
logger.info(f"�?{len(base_retriever.documents)}�?문서 �?��?�서 ?�스 목록 ?�성 �?..")
|
|
|
514 |
for doc in base_retriever.documents:
|
515 |
+
# 문서 �?��가 ?�셔?�리 ?�태?�고 가?? if not isinstance(doc, dict): continue
|
|
|
516 |
|
517 |
+
source = doc.get("source", "unknown") # 메�??�이?�에??source 가?�오�? if source == "unknown" and "metadata" in doc and isinstance(doc["metadata"], dict):
|
|
|
518 |
source = doc["metadata"].get("source", "unknown") # Langchain Document 구조 고려
|
519 |
|
520 |
if source != "unknown":
|
521 |
if source in sources:
|
522 |
sources[source]["chunks"] += 1
|
523 |
else:
|
524 |
+
# 메�??�이?�에??추�? ?�보 가?�오�? filename = doc.get("filename", source)
|
|
|
525 |
filetype = doc.get("filetype", "unknown")
|
526 |
if "metadata" in doc and isinstance(doc["metadata"], dict):
|
527 |
filename = doc["metadata"].get("filename", filename)
|
|
|
534 |
}
|
535 |
total_chunks += 1
|
536 |
else:
|
537 |
+
logger.info("검?�기??문서가 ?�거??documents ?�성??찾을 ???�습?�다.")
|
538 |
|
539 |
+
# 목록 ?�식 변??�??�렬
|
540 |
documents = [{"source": src, **info} for src, info in sources.items()]
|
541 |
documents.sort(key=lambda x: x["chunks"], reverse=True)
|
542 |
|
543 |
+
logger.info(f"문서 목록 조회 ?�료: {len(documents)}�??�스 ?�일, {total_chunks}�?�?��")
|
544 |
return jsonify({
|
545 |
"documents": documents,
|
546 |
"total_documents": len(documents),
|
|
|
548 |
})
|
549 |
|
550 |
except Exception as e:
|
551 |
+
logger.error(f"문서 목록 조회 �??�류 발생: {e}", exc_info=True)
|
552 |
+
return jsonify({"error": f"문서 목록 조회 �??�류: {str(e)}"}), 500
|
app/static/js/app-device.js
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
/**
|
2 |
-
* RAG
|
3 |
*/
|
4 |
|
5 |
-
// DOM
|
6 |
const deviceTab = document.getElementById('deviceTab');
|
7 |
const deviceSection = document.getElementById('deviceSection');
|
8 |
const checkDeviceStatusButton = document.getElementById('checkDeviceStatusButton');
|
@@ -19,118 +19,110 @@ const programsLoading = document.getElementById('programsLoading');
|
|
19 |
const programsList = document.getElementById('programsList');
|
20 |
const noProgramsMessage = document.getElementById('noProgramsMessage');
|
21 |
|
22 |
-
//
|
23 |
-
// 직접
|
24 |
-
let DEVICE_SERVER_URL = ''; //
|
25 |
-
|
26 |
/**
|
27 |
-
*
|
28 |
-
* @param {string} endpoint - API
|
29 |
-
* @returns {string} -
|
30 |
*/
|
31 |
function getDeviceApiUrl(endpoint) {
|
32 |
-
// 직접
|
33 |
if (DEVICE_SERVER_URL) {
|
34 |
return `${DEVICE_SERVER_URL}${endpoint}`;
|
35 |
}
|
36 |
|
37 |
-
//
|
38 |
return endpoint;
|
39 |
}
|
40 |
|
41 |
/**
|
42 |
-
*
|
43 |
*/
|
44 |
function initDeviceServerSettings() {
|
45 |
-
console.log("
|
46 |
-
//
|
47 |
fetch('/api/device/settings')
|
48 |
.then(response => {
|
49 |
if (!response.ok) {
|
50 |
-
throw new Error('
|
51 |
}
|
52 |
return response.json();
|
53 |
})
|
54 |
.then(data => {
|
55 |
if (data.server_url) {
|
56 |
-
console.log(
|
57 |
DEVICE_SERVER_URL = data.server_url;
|
58 |
} else {
|
59 |
-
//
|
60 |
const currentHost = window.location.hostname;
|
61 |
const protocol = window.location.protocol;
|
62 |
DEVICE_SERVER_URL = `${protocol}//${currentHost}:5051`;
|
63 |
-
console.log(
|
64 |
}
|
65 |
})
|
66 |
.catch(error => {
|
67 |
-
console.error('
|
68 |
-
//
|
69 |
const currentHost = window.location.hostname;
|
70 |
const protocol = window.location.protocol;
|
71 |
DEVICE_SERVER_URL = `${protocol}//${currentHost}:5051`;
|
72 |
-
console.log(
|
73 |
});
|
74 |
}
|
75 |
|
76 |
-
//
|
77 |
-
|
78 |
-
console.log("장치 관리 모듈 초기화");
|
79 |
|
80 |
-
//
|
81 |
-
initDeviceServerSettings();
|
82 |
|
83 |
-
//
|
84 |
-
//
|
85 |
if (typeof window.switchTab !== 'function') {
|
86 |
-
console.log("window.switchTab
|
87 |
} else {
|
88 |
-
console.log("window.switchTab
|
89 |
}
|
90 |
|
91 |
-
//
|
92 |
-
|
93 |
-
console.log("장치 상태 확인 버튼 클릭");
|
94 |
checkDeviceStatus();
|
95 |
});
|
96 |
|
97 |
-
//
|
98 |
-
|
99 |
-
console.log("장치 목록 새로고침 버튼 클릭");
|
100 |
loadDevices();
|
101 |
});
|
102 |
|
103 |
-
//
|
104 |
-
|
105 |
-
console.log("프로그램 목록 로드 버튼 클릭");
|
106 |
loadPrograms();
|
107 |
});
|
108 |
});
|
109 |
|
110 |
/**
|
111 |
-
*
|
112 |
-
* @param {Error} error -
|
113 |
-
* @returns {string} -
|
114 |
*/
|
115 |
function handleError(error) {
|
116 |
-
console.error("
|
117 |
|
118 |
if (error.name === 'AbortError') {
|
119 |
-
return '
|
120 |
}
|
121 |
|
122 |
if (error.message && (error.message.includes('NetworkError') || error.message.includes('Failed to fetch'))) {
|
123 |
-
return '
|
124 |
}
|
125 |
|
126 |
-
return
|
127 |
}
|
128 |
|
129 |
/**
|
130 |
-
* HTML
|
131 |
-
* @param {string} unsafe -
|
132 |
-
* @returns {string} - 이스케이프 후 문자열
|
133 |
-
*/
|
134 |
function escapeHtml(unsafe) {
|
135 |
if (typeof unsafe !== 'string') return unsafe;
|
136 |
return unsafe
|
@@ -142,55 +134,54 @@ function escapeHtml(unsafe) {
|
|
142 |
}
|
143 |
|
144 |
/**
|
145 |
-
*
|
146 |
*/
|
147 |
async function checkDeviceStatus() {
|
148 |
-
console.log("
|
149 |
|
150 |
-
// UI
|
151 |
deviceStatusResult.classList.add('hidden');
|
152 |
deviceStatusLoading.classList.remove('hidden');
|
153 |
|
154 |
try {
|
155 |
-
//
|
156 |
const controller = new AbortController();
|
157 |
-
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5
|
158 |
-
|
159 |
-
// API 요청 - 수정된 URL 구성 사용
|
160 |
const response = await fetch(getDeviceApiUrl('/api/status'), {
|
161 |
signal: controller.signal
|
162 |
});
|
163 |
|
164 |
-
clearTimeout(timeoutId); //
|
165 |
|
166 |
-
//
|
167 |
if (!response.ok) {
|
168 |
-
throw new Error(`HTTP
|
169 |
}
|
170 |
|
171 |
const data = await response.json();
|
172 |
-
console.log("
|
173 |
|
174 |
-
// UI
|
175 |
deviceStatusLoading.classList.add('hidden');
|
176 |
deviceStatusResult.classList.remove('hidden');
|
177 |
|
178 |
if (data.status === "online") {
|
179 |
-
//
|
180 |
statusIcon.innerHTML = '<i class="fas fa-circle online"></i>';
|
181 |
-
statusText.textContent =
|
182 |
|
183 |
-
//
|
184 |
loadDevices();
|
185 |
} else {
|
186 |
-
//
|
187 |
statusIcon.innerHTML = '<i class="fas fa-circle offline"></i>';
|
188 |
-
statusText.textContent =
|
189 |
}
|
190 |
} catch (error) {
|
191 |
-
console.error("
|
192 |
|
193 |
-
// UI
|
194 |
deviceStatusLoading.classList.add('hidden');
|
195 |
deviceStatusResult.classList.remove('hidden');
|
196 |
|
@@ -200,53 +191,52 @@ async function checkDeviceStatus() {
|
|
200 |
}
|
201 |
|
202 |
/**
|
203 |
-
*
|
204 |
*/
|
205 |
async function loadDevices() {
|
206 |
-
console.log("
|
207 |
|
208 |
-
// UI
|
209 |
deviceList.innerHTML = '';
|
210 |
noDevicesMessage.classList.add('hidden');
|
211 |
devicesLoading.classList.remove('hidden');
|
212 |
|
213 |
try {
|
214 |
-
//
|
215 |
const controller = new AbortController();
|
216 |
-
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5
|
217 |
-
|
218 |
-
// API 요청 - 수정된 URL 구성 사용
|
219 |
const response = await fetch(getDeviceApiUrl('/api/devices'), {
|
220 |
signal: controller.signal
|
221 |
});
|
222 |
|
223 |
-
clearTimeout(timeoutId); //
|
224 |
|
225 |
-
//
|
226 |
if (!response.ok) {
|
227 |
-
throw new Error(`HTTP
|
228 |
}
|
229 |
|
230 |
const data = await response.json();
|
231 |
-
console.log("
|
232 |
|
233 |
-
// UI
|
234 |
devicesLoading.classList.add('hidden');
|
235 |
|
236 |
if (data.devices && data.devices.length > 0) {
|
237 |
-
//
|
238 |
data.devices.forEach(device => {
|
239 |
const deviceElement = createDeviceItem(device);
|
240 |
deviceList.appendChild(deviceElement);
|
241 |
});
|
242 |
} else {
|
243 |
-
//
|
244 |
noDevicesMessage.classList.remove('hidden');
|
245 |
}
|
246 |
} catch (error) {
|
247 |
-
console.error("
|
248 |
|
249 |
-
// UI
|
250 |
devicesLoading.classList.add('hidden');
|
251 |
noDevicesMessage.classList.remove('hidden');
|
252 |
noDevicesMessage.textContent = handleError(error);
|
@@ -254,61 +244,61 @@ async function loadDevices() {
|
|
254 |
}
|
255 |
|
256 |
/**
|
257 |
-
*
|
258 |
-
* @param {Object} device -
|
259 |
-
* @returns {HTMLElement} -
|
260 |
*/
|
261 |
function createDeviceItem(device) {
|
262 |
const deviceItem = document.createElement('div');
|
263 |
deviceItem.classList.add('device-item');
|
264 |
|
265 |
-
//
|
266 |
-
if (device.status === 'online' || device.status === '
|
267 |
deviceItem.classList.add('online');
|
268 |
-
} else if (device.status === 'offline' || device.status === '
|
269 |
deviceItem.classList.add('offline');
|
270 |
} else if (device.status === 'warning' || device.status === '경고') {
|
271 |
deviceItem.classList.add('warning');
|
272 |
}
|
273 |
|
274 |
-
//
|
275 |
const deviceHeader = document.createElement('div');
|
276 |
deviceHeader.classList.add('device-item-header');
|
277 |
|
278 |
const deviceName = document.createElement('div');
|
279 |
deviceName.classList.add('device-name');
|
280 |
-
deviceName.textContent = device.name || '
|
281 |
|
282 |
const deviceStatusBadge = document.createElement('div');
|
283 |
deviceStatusBadge.classList.add('device-status-badge');
|
284 |
|
285 |
-
//
|
286 |
-
if (device.status === 'online' || device.status === '
|
287 |
deviceStatusBadge.classList.add('online');
|
288 |
-
deviceStatusBadge.textContent = '
|
289 |
-
} else if (device.status === 'offline' || device.status === '
|
290 |
deviceStatusBadge.classList.add('offline');
|
291 |
-
deviceStatusBadge.textContent = '
|
292 |
} else if (device.status === 'warning' || device.status === '경고') {
|
293 |
deviceStatusBadge.classList.add('warning');
|
294 |
deviceStatusBadge.textContent = '경고';
|
295 |
} else {
|
296 |
-
deviceStatusBadge.textContent = device.status || '
|
297 |
}
|
298 |
|
299 |
deviceHeader.appendChild(deviceName);
|
300 |
deviceHeader.appendChild(deviceStatusBadge);
|
301 |
|
302 |
-
//
|
303 |
const deviceInfo = document.createElement('div');
|
304 |
deviceInfo.classList.add('device-info');
|
305 |
-
deviceInfo.textContent =
|
306 |
|
307 |
-
//
|
308 |
const deviceDetails = document.createElement('div');
|
309 |
deviceDetails.classList.add('device-details');
|
310 |
|
311 |
-
//
|
312 |
if (device.ip) {
|
313 |
deviceDetails.textContent += `IP: ${device.ip}`;
|
314 |
}
|
@@ -316,10 +306,10 @@ function createDeviceItem(device) {
|
|
316 |
deviceDetails.textContent += (deviceDetails.textContent ? ', ' : '') + `MAC: ${device.mac}`;
|
317 |
}
|
318 |
if (device.lastSeen) {
|
319 |
-
deviceDetails.textContent += (deviceDetails.textContent ? ', ' : '') +
|
320 |
}
|
321 |
|
322 |
-
//
|
323 |
deviceItem.appendChild(deviceHeader);
|
324 |
deviceItem.appendChild(deviceInfo);
|
325 |
|
@@ -331,53 +321,52 @@ function createDeviceItem(device) {
|
|
331 |
}
|
332 |
|
333 |
/**
|
334 |
-
*
|
335 |
*/
|
336 |
async function loadPrograms() {
|
337 |
-
console.log("
|
338 |
|
339 |
-
// UI
|
340 |
programsList.innerHTML = '';
|
341 |
noProgramsMessage.classList.add('hidden');
|
342 |
programsLoading.classList.remove('hidden');
|
343 |
|
344 |
try {
|
345 |
-
//
|
346 |
const controller = new AbortController();
|
347 |
-
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5
|
348 |
-
|
349 |
-
// API 요청 - 수정된 URL 구성 사용
|
350 |
const response = await fetch(getDeviceApiUrl('/api/programs'), {
|
351 |
signal: controller.signal
|
352 |
});
|
353 |
|
354 |
-
clearTimeout(timeoutId); //
|
355 |
|
356 |
-
//
|
357 |
if (!response.ok) {
|
358 |
-
throw new Error(`HTTP
|
359 |
}
|
360 |
|
361 |
const data = await response.json();
|
362 |
-
console.log("
|
363 |
|
364 |
-
// UI
|
365 |
programsLoading.classList.add('hidden');
|
366 |
|
367 |
if (data.programs && data.programs.length > 0) {
|
368 |
-
//
|
369 |
data.programs.forEach(program => {
|
370 |
const programElement = createProgramItem(program);
|
371 |
programsList.appendChild(programElement);
|
372 |
});
|
373 |
} else {
|
374 |
-
//
|
375 |
noProgramsMessage.classList.remove('hidden');
|
376 |
}
|
377 |
} catch (error) {
|
378 |
-
console.error("
|
379 |
|
380 |
-
// UI
|
381 |
programsLoading.classList.add('hidden');
|
382 |
noProgramsMessage.classList.remove('hidden');
|
383 |
noProgramsMessage.textContent = handleError(error);
|
@@ -385,25 +374,25 @@ async function loadPrograms() {
|
|
385 |
}
|
386 |
|
387 |
/**
|
388 |
-
*
|
389 |
-
* @param {Object} program -
|
390 |
-
* @returns {HTMLElement} -
|
391 |
*/
|
392 |
function createProgramItem(program) {
|
393 |
const programItem = document.createElement('div');
|
394 |
programItem.classList.add('program-item');
|
395 |
|
396 |
-
//
|
397 |
const programHeader = document.createElement('div');
|
398 |
programHeader.classList.add('program-item-header');
|
399 |
|
400 |
const programName = document.createElement('div');
|
401 |
programName.classList.add('program-name');
|
402 |
-
programName.textContent = program.name || '
|
403 |
|
404 |
programHeader.appendChild(programName);
|
405 |
|
406 |
-
//
|
407 |
if (program.description) {
|
408 |
const programDescription = document.createElement('div');
|
409 |
programDescription.classList.add('program-description');
|
@@ -411,15 +400,15 @@ function createProgramItem(program) {
|
|
411 |
programItem.appendChild(programDescription);
|
412 |
}
|
413 |
|
414 |
-
//
|
415 |
const executeButton = document.createElement('button');
|
416 |
executeButton.classList.add('execute-btn');
|
417 |
-
executeButton.textContent = '
|
418 |
executeButton.addEventListener('click', () => {
|
419 |
executeProgram(program.id, program.name);
|
420 |
});
|
421 |
|
422 |
-
//
|
423 |
programItem.appendChild(programHeader);
|
424 |
programItem.appendChild(executeButton);
|
425 |
|
@@ -427,72 +416,72 @@ function createProgramItem(program) {
|
|
427 |
}
|
428 |
|
429 |
/**
|
430 |
-
*
|
431 |
-
* @param {string} programId -
|
432 |
-
* @param {string} programName -
|
433 |
*/
|
434 |
async function executeProgram(programId, programName) {
|
435 |
if (!programId) return;
|
436 |
|
437 |
-
console.log(
|
438 |
|
439 |
-
//
|
440 |
-
if (!confirm(`'${programName}'
|
441 |
-
console.log('
|
442 |
return;
|
443 |
}
|
444 |
|
445 |
try {
|
446 |
-
// 로딩
|
447 |
-
showNotification(`'${programName}'
|
448 |
|
449 |
-
// API
|
450 |
const response = await fetch(getDeviceApiUrl(`/api/programs/${programId}/execute`), {
|
451 |
method: 'POST',
|
452 |
headers: {
|
453 |
'Content-Type': 'application/json'
|
454 |
},
|
455 |
-
body: JSON.stringify({}) //
|
456 |
});
|
457 |
|
458 |
-
//
|
459 |
if (!response.ok) {
|
460 |
-
throw new Error(`HTTP
|
461 |
}
|
462 |
|
463 |
const data = await response.json();
|
464 |
-
console.log(
|
465 |
|
466 |
// 결과 처리
|
467 |
if (data.success) {
|
468 |
-
showNotification(`'${programName}'
|
469 |
} else {
|
470 |
-
showNotification(`'${programName}'
|
471 |
}
|
472 |
} catch (error) {
|
473 |
-
console.error(
|
474 |
-
showNotification(`'${programName}'
|
475 |
}
|
476 |
}
|
477 |
|
478 |
/**
|
479 |
-
*
|
480 |
-
* @param {string} message -
|
481 |
-
* @param {string} type -
|
482 |
*/
|
483 |
function showNotification(message, type = 'info') {
|
484 |
-
// 기존
|
485 |
const existingNotification = document.querySelector('.notification');
|
486 |
if (existingNotification) {
|
487 |
existingNotification.remove();
|
488 |
}
|
489 |
|
490 |
-
//
|
491 |
const notification = document.createElement('div');
|
492 |
notification.classList.add('notification', type);
|
493 |
notification.textContent = message;
|
494 |
|
495 |
-
//
|
496 |
const closeButton = document.createElement('button');
|
497 |
closeButton.classList.add('notification-close');
|
498 |
closeButton.innerHTML = '×';
|
@@ -502,16 +491,15 @@ function showNotification(message, type = 'info') {
|
|
502 |
|
503 |
notification.appendChild(closeButton);
|
504 |
|
505 |
-
//
|
506 |
document.body.appendChild(notification);
|
507 |
|
508 |
-
//
|
509 |
setTimeout(() => {
|
510 |
if (document.body.contains(notification)) {
|
511 |
notification.remove();
|
512 |
}
|
513 |
-
}, 5000); // 5
|
514 |
-
}
|
515 |
|
516 |
-
// checkDeviceStatus
|
517 |
-
window.checkDeviceStatus = checkDeviceStatus;
|
|
|
1 |
/**
|
2 |
+
* RAG 검??챗봇 ?�치 관�?JavaScript
|
3 |
*/
|
4 |
|
5 |
+
// DOM ?�소
|
6 |
const deviceTab = document.getElementById('deviceTab');
|
7 |
const deviceSection = document.getElementById('deviceSection');
|
8 |
const checkDeviceStatusButton = document.getElementById('checkDeviceStatusButton');
|
|
|
19 |
const programsList = document.getElementById('programsList');
|
20 |
const noProgramsMessage = document.getElementById('noProgramsMessage');
|
21 |
|
22 |
+
// ?�치 ?�버 URL ?�정
|
23 |
+
// 직접 ?�근 ?�정: �?문자?�이�??�재 ?�스?�의 5051 ?�트�??�용
|
24 |
+
let DEVICE_SERVER_URL = ''; // ?�버?�서 ?�달??URL???�으�?초기?????�정??
|
|
|
25 |
/**
|
26 |
+
* ?�치 ?�버 API 경로 ?�성 ?�수
|
27 |
+
* @param {string} endpoint - API ?�드?�인??경로
|
28 |
+
* @returns {string} - ?�전??API URL
|
29 |
*/
|
30 |
function getDeviceApiUrl(endpoint) {
|
31 |
+
// 직접 ?�근 모드??경우 (별도 ?�버??직접 ?�청)
|
32 |
if (DEVICE_SERVER_URL) {
|
33 |
return `${DEVICE_SERVER_URL}${endpoint}`;
|
34 |
}
|
35 |
|
36 |
+
// ?�록??모드??경우 (?��? API 경로 ?�용)
|
37 |
return endpoint;
|
38 |
}
|
39 |
|
40 |
/**
|
41 |
+
* ?�치 ?�버 ?�정 초기??(?�이지 로드 ???�출)
|
42 |
*/
|
43 |
function initDeviceServerSettings() {
|
44 |
+
console.log("?�치 ?�버 ?�정 초기???�작");
|
45 |
+
// ?�버 URL ?�정 (???�버로�????�정 불러?�기)
|
46 |
fetch('/api/device/settings')
|
47 |
.then(response => {
|
48 |
if (!response.ok) {
|
49 |
+
throw new Error('?�정??가?�올 ???�습?�다');
|
50 |
}
|
51 |
return response.json();
|
52 |
})
|
53 |
.then(data => {
|
54 |
if (data.server_url) {
|
55 |
+
console.log(`?�치 ?�버 URL ?�정?? ${data.server_url}`);
|
56 |
DEVICE_SERVER_URL = data.server_url;
|
57 |
} else {
|
58 |
+
// ?�정 ?�음 - ?�동 ?�성 (?�재 ?�스??+ ?�트 5051)
|
59 |
const currentHost = window.location.hostname;
|
60 |
const protocol = window.location.protocol;
|
61 |
DEVICE_SERVER_URL = `${protocol}//${currentHost}:5051`;
|
62 |
+
console.log(`?�치 ?�버 URL ?�동 ?�정: ${DEVICE_SERVER_URL}`);
|
63 |
}
|
64 |
})
|
65 |
.catch(error => {
|
66 |
+
console.error('?�치 ?�버 ?�정 초기???�류:', error);
|
67 |
+
// 기본값으�??�정 (?�재 ?�스??+ ?�트 5051)
|
68 |
const currentHost = window.location.hostname;
|
69 |
const protocol = window.location.protocol;
|
70 |
DEVICE_SERVER_URL = `${protocol}//${currentHost}:5051`;
|
71 |
+
console.log(`?�치 ?�버 URL 기본�??�정: ${DEVICE_SERVER_URL}`);
|
72 |
});
|
73 |
}
|
74 |
|
75 |
+
// ?�이지 로드 ??초기??document.addEventListener('DOMContentLoaded', () => {
|
76 |
+
console.log("?�치 관�?모듈 초기??);
|
|
|
77 |
|
78 |
+
// ?�치 ?�버 ?�정 초기?? initDeviceServerSettings();
|
|
|
79 |
|
80 |
+
// ???�환 ?�벤??리스?�는 ?��? app.js?�서 ?�록?�어 ?�으므�??�기?�는 ?�록?��? ?�음
|
81 |
+
// ?�???�역 ?�수가 ?�바르게 ?�정?�어 ?�는지 ?�인
|
82 |
if (typeof window.switchTab !== 'function') {
|
83 |
+
console.log("window.switchTab ?�수가 ?�의?��? ?�았?�니?? ?��? 구현???�용?�니??");
|
84 |
} else {
|
85 |
+
console.log("window.switchTab ?�수 ?�용 가?�합?�다.");
|
86 |
}
|
87 |
|
88 |
+
// ?�치 ?�태 ?�인 버튼 ?�벤??리스?? checkDeviceStatusButton.addEventListener('click', () => {
|
89 |
+
console.log("?�치 ?�태 ?�인 버튼 ?�릭");
|
|
|
90 |
checkDeviceStatus();
|
91 |
});
|
92 |
|
93 |
+
// ?�치 목록 ?�로고침 버튼 ?�벤??리스?? refreshDevicesButton.addEventListener('click', () => {
|
94 |
+
console.log("?�치 목록 ?�로고침 버튼 ?�릭");
|
|
|
95 |
loadDevices();
|
96 |
});
|
97 |
|
98 |
+
// ?�로그램 목록 로드 버튼 ?�벤??리스?? loadProgramsButton.addEventListener('click', () => {
|
99 |
+
console.log("?�로그램 목록 로드 버튼 ?�릭");
|
|
|
100 |
loadPrograms();
|
101 |
});
|
102 |
});
|
103 |
|
104 |
/**
|
105 |
+
* ?�러 처리 ?�퍼 ?�수
|
106 |
+
* @param {Error} error - 발생???�류
|
107 |
+
* @returns {string} - ?�용?�에�??�시???�류 메시지
|
108 |
*/
|
109 |
function handleError(error) {
|
110 |
+
console.error("?�류 발생:", error);
|
111 |
|
112 |
if (error.name === 'AbortError') {
|
113 |
+
return '?�청 ?�간??초과?�었?�니?? ?�버가 ?�답?��? ?�습?�다.';
|
114 |
}
|
115 |
|
116 |
if (error.message && (error.message.includes('NetworkError') || error.message.includes('Failed to fetch'))) {
|
117 |
+
return '?�트?�크 ?�류가 발생?�습?�다. ?�버???�결?????�습?�다.';
|
118 |
}
|
119 |
|
120 |
+
return `?�류가 발생?�습?�다: ${error.message || '?????�는 ?�류'}`;
|
121 |
}
|
122 |
|
123 |
/**
|
124 |
+
* HTML ?�스케?�프 ?�수 (XSS 방�?)
|
125 |
+
* @param {string} unsafe - ?�스케?�프 ??문자?? * @returns {string} - ?�스케?�프 ??문자?? */
|
|
|
|
|
126 |
function escapeHtml(unsafe) {
|
127 |
if (typeof unsafe !== 'string') return unsafe;
|
128 |
return unsafe
|
|
|
134 |
}
|
135 |
|
136 |
/**
|
137 |
+
* ?�치 관�??�버 ?�태 ?�인 ?�수 - ?�역 ?�수�?export
|
138 |
*/
|
139 |
async function checkDeviceStatus() {
|
140 |
+
console.log("?�치 ?�태 ?�인 �?..");
|
141 |
|
142 |
+
// UI ?�데?�트
|
143 |
deviceStatusResult.classList.add('hidden');
|
144 |
deviceStatusLoading.classList.remove('hidden');
|
145 |
|
146 |
try {
|
147 |
+
// ?�?�아???�정???�한 컨트롤러
|
148 |
const controller = new AbortController();
|
149 |
+
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5�??�?�아??
|
150 |
+
// API ?�청 - ?�정??URL 구성 ?�용
|
|
|
151 |
const response = await fetch(getDeviceApiUrl('/api/status'), {
|
152 |
signal: controller.signal
|
153 |
});
|
154 |
|
155 |
+
clearTimeout(timeoutId); // ?�?�아???�제
|
156 |
|
157 |
+
// ?�답 처리
|
158 |
if (!response.ok) {
|
159 |
+
throw new Error(`HTTP ?�류: ${response.status}`);
|
160 |
}
|
161 |
|
162 |
const data = await response.json();
|
163 |
+
console.log("?�치 ?�태 ?�답:", data);
|
164 |
|
165 |
+
// UI ?�데?�트
|
166 |
deviceStatusLoading.classList.add('hidden');
|
167 |
deviceStatusResult.classList.remove('hidden');
|
168 |
|
169 |
if (data.status === "online") {
|
170 |
+
// ?�라???�태??경우
|
171 |
statusIcon.innerHTML = '<i class="fas fa-circle online"></i>';
|
172 |
+
statusText.textContent = `?�버 ?�태: ${data.status || '?�상'}`;
|
173 |
|
174 |
+
// ?�동?�로 ?�치 목록 로드
|
175 |
loadDevices();
|
176 |
} else {
|
177 |
+
// ?�프?�인 ?�는 ?�류 ?�태??경우
|
178 |
statusIcon.innerHTML = '<i class="fas fa-circle offline"></i>';
|
179 |
+
statusText.textContent = `?�버 ?�류: ${data.error || '?????�는 ?�류'}`;
|
180 |
}
|
181 |
} catch (error) {
|
182 |
+
console.error("?�치 ?�태 ?�인 ?�류:", error);
|
183 |
|
184 |
+
// UI ?�데?�트
|
185 |
deviceStatusLoading.classList.add('hidden');
|
186 |
deviceStatusResult.classList.remove('hidden');
|
187 |
|
|
|
191 |
}
|
192 |
|
193 |
/**
|
194 |
+
* ?�치 목록 로드 ?�수
|
195 |
*/
|
196 |
async function loadDevices() {
|
197 |
+
console.log("?�치 목록 로드 �?..");
|
198 |
|
199 |
+
// UI ?�데?�트
|
200 |
deviceList.innerHTML = '';
|
201 |
noDevicesMessage.classList.add('hidden');
|
202 |
devicesLoading.classList.remove('hidden');
|
203 |
|
204 |
try {
|
205 |
+
// ?�?�아???�정???�한 컨트롤러
|
206 |
const controller = new AbortController();
|
207 |
+
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5�??�?�아??
|
208 |
+
// API ?�청 - ?�정??URL 구성 ?�용
|
|
|
209 |
const response = await fetch(getDeviceApiUrl('/api/devices'), {
|
210 |
signal: controller.signal
|
211 |
});
|
212 |
|
213 |
+
clearTimeout(timeoutId); // ?�?�아???�제
|
214 |
|
215 |
+
// ?�답 처리
|
216 |
if (!response.ok) {
|
217 |
+
throw new Error(`HTTP ?�류: ${response.status}`);
|
218 |
}
|
219 |
|
220 |
const data = await response.json();
|
221 |
+
console.log("?�치 목록 ?�답:", data);
|
222 |
|
223 |
+
// UI ?�데?�트
|
224 |
devicesLoading.classList.add('hidden');
|
225 |
|
226 |
if (data.devices && data.devices.length > 0) {
|
227 |
+
// ?�치 목록 ?�시
|
228 |
data.devices.forEach(device => {
|
229 |
const deviceElement = createDeviceItem(device);
|
230 |
deviceList.appendChild(deviceElement);
|
231 |
});
|
232 |
} else {
|
233 |
+
// ?�치 ?�음 메시지 ?�시
|
234 |
noDevicesMessage.classList.remove('hidden');
|
235 |
}
|
236 |
} catch (error) {
|
237 |
+
console.error("?�치 목록 로드 ?�류:", error);
|
238 |
|
239 |
+
// UI ?�데?�트
|
240 |
devicesLoading.classList.add('hidden');
|
241 |
noDevicesMessage.classList.remove('hidden');
|
242 |
noDevicesMessage.textContent = handleError(error);
|
|
|
244 |
}
|
245 |
|
246 |
/**
|
247 |
+
* ?�치 ?�이???�성 ?�수
|
248 |
+
* @param {Object} device - ?�치 ?�보 객체
|
249 |
+
* @returns {HTMLElement} - ?�치 ?�이??DOM ?�소
|
250 |
*/
|
251 |
function createDeviceItem(device) {
|
252 |
const deviceItem = document.createElement('div');
|
253 |
deviceItem.classList.add('device-item');
|
254 |
|
255 |
+
// ?�태???�른 ?�래??추�?
|
256 |
+
if (device.status === 'online' || device.status === '?�라??) {
|
257 |
deviceItem.classList.add('online');
|
258 |
+
} else if (device.status === 'offline' || device.status === '?�프?�인') {
|
259 |
deviceItem.classList.add('offline');
|
260 |
} else if (device.status === 'warning' || device.status === '경고') {
|
261 |
deviceItem.classList.add('warning');
|
262 |
}
|
263 |
|
264 |
+
// ?�치 ?�더 (?�름 �??�태)
|
265 |
const deviceHeader = document.createElement('div');
|
266 |
deviceHeader.classList.add('device-item-header');
|
267 |
|
268 |
const deviceName = document.createElement('div');
|
269 |
deviceName.classList.add('device-name');
|
270 |
+
deviceName.textContent = device.name || '?????�는 ?�치';
|
271 |
|
272 |
const deviceStatusBadge = document.createElement('div');
|
273 |
deviceStatusBadge.classList.add('device-status-badge');
|
274 |
|
275 |
+
// ?�태???�른 배�? ?�정
|
276 |
+
if (device.status === 'online' || device.status === '?�라??) {
|
277 |
deviceStatusBadge.classList.add('online');
|
278 |
+
deviceStatusBadge.textContent = '?�라??;
|
279 |
+
} else if (device.status === 'offline' || device.status === '?�프?�인') {
|
280 |
deviceStatusBadge.classList.add('offline');
|
281 |
+
deviceStatusBadge.textContent = '?�프?�인';
|
282 |
} else if (device.status === 'warning' || device.status === '경고') {
|
283 |
deviceStatusBadge.classList.add('warning');
|
284 |
deviceStatusBadge.textContent = '경고';
|
285 |
} else {
|
286 |
+
deviceStatusBadge.textContent = device.status || '?????�음';
|
287 |
}
|
288 |
|
289 |
deviceHeader.appendChild(deviceName);
|
290 |
deviceHeader.appendChild(deviceStatusBadge);
|
291 |
|
292 |
+
// ?�치 ?�보
|
293 |
const deviceInfo = document.createElement('div');
|
294 |
deviceInfo.classList.add('device-info');
|
295 |
+
deviceInfo.textContent = `?�형: ${device.type || '?????�음'}`;
|
296 |
|
297 |
+
// ?�치 ?��? ?�보
|
298 |
const deviceDetails = document.createElement('div');
|
299 |
deviceDetails.classList.add('device-details');
|
300 |
|
301 |
+
// 추�? ?�보가 ?�는 경우 ?�시
|
302 |
if (device.ip) {
|
303 |
deviceDetails.textContent += `IP: ${device.ip}`;
|
304 |
}
|
|
|
306 |
deviceDetails.textContent += (deviceDetails.textContent ? ', ' : '') + `MAC: ${device.mac}`;
|
307 |
}
|
308 |
if (device.lastSeen) {
|
309 |
+
deviceDetails.textContent += (deviceDetails.textContent ? ', ' : '') + `마�?�??�동: ${device.lastSeen}`;
|
310 |
}
|
311 |
|
312 |
+
// ?�이?�에 ?�소 추�?
|
313 |
deviceItem.appendChild(deviceHeader);
|
314 |
deviceItem.appendChild(deviceInfo);
|
315 |
|
|
|
321 |
}
|
322 |
|
323 |
/**
|
324 |
+
* ?�로그램 목록 로드 ?�수
|
325 |
*/
|
326 |
async function loadPrograms() {
|
327 |
+
console.log("?�로그램 목록 로드 �?..");
|
328 |
|
329 |
+
// UI ?�데?�트
|
330 |
programsList.innerHTML = '';
|
331 |
noProgramsMessage.classList.add('hidden');
|
332 |
programsLoading.classList.remove('hidden');
|
333 |
|
334 |
try {
|
335 |
+
// ?�?�아???�정???�한 컨트롤러
|
336 |
const controller = new AbortController();
|
337 |
+
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5�??�?�아??
|
338 |
+
// API ?�청 - ?�정??URL 구성 ?�용
|
|
|
339 |
const response = await fetch(getDeviceApiUrl('/api/programs'), {
|
340 |
signal: controller.signal
|
341 |
});
|
342 |
|
343 |
+
clearTimeout(timeoutId); // ?�?�아???�제
|
344 |
|
345 |
+
// ?�답 처리
|
346 |
if (!response.ok) {
|
347 |
+
throw new Error(`HTTP ?�류: ${response.status}`);
|
348 |
}
|
349 |
|
350 |
const data = await response.json();
|
351 |
+
console.log("?�로그램 목록 ?�답:", data);
|
352 |
|
353 |
+
// UI ?�데?�트
|
354 |
programsLoading.classList.add('hidden');
|
355 |
|
356 |
if (data.programs && data.programs.length > 0) {
|
357 |
+
// ?�로그램 목록 ?�시
|
358 |
data.programs.forEach(program => {
|
359 |
const programElement = createProgramItem(program);
|
360 |
programsList.appendChild(programElement);
|
361 |
});
|
362 |
} else {
|
363 |
+
// ?�로그램 ?�음 메시지 ?�시
|
364 |
noProgramsMessage.classList.remove('hidden');
|
365 |
}
|
366 |
} catch (error) {
|
367 |
+
console.error("?�로그램 목록 로드 ?�류:", error);
|
368 |
|
369 |
+
// UI ?�데?�트
|
370 |
programsLoading.classList.add('hidden');
|
371 |
noProgramsMessage.classList.remove('hidden');
|
372 |
noProgramsMessage.textContent = handleError(error);
|
|
|
374 |
}
|
375 |
|
376 |
/**
|
377 |
+
* ?�로그램 ?�이???�성 ?�수
|
378 |
+
* @param {Object} program - ?�로그램 ?�보 객체
|
379 |
+
* @returns {HTMLElement} - ?�로그램 ?�이??DOM ?�소
|
380 |
*/
|
381 |
function createProgramItem(program) {
|
382 |
const programItem = document.createElement('div');
|
383 |
programItem.classList.add('program-item');
|
384 |
|
385 |
+
// ?�로그램 ?�더 (?�름)
|
386 |
const programHeader = document.createElement('div');
|
387 |
programHeader.classList.add('program-item-header');
|
388 |
|
389 |
const programName = document.createElement('div');
|
390 |
programName.classList.add('program-name');
|
391 |
+
programName.textContent = program.name || '?????�는 ?�로그램';
|
392 |
|
393 |
programHeader.appendChild(programName);
|
394 |
|
395 |
+
// ?�로그램 ?�명
|
396 |
if (program.description) {
|
397 |
const programDescription = document.createElement('div');
|
398 |
programDescription.classList.add('program-description');
|
|
|
400 |
programItem.appendChild(programDescription);
|
401 |
}
|
402 |
|
403 |
+
// ?�행 버튼
|
404 |
const executeButton = document.createElement('button');
|
405 |
executeButton.classList.add('execute-btn');
|
406 |
+
executeButton.textContent = '?�행';
|
407 |
executeButton.addEventListener('click', () => {
|
408 |
executeProgram(program.id, program.name);
|
409 |
});
|
410 |
|
411 |
+
// ?�이?�에 ?�소 추�?
|
412 |
programItem.appendChild(programHeader);
|
413 |
programItem.appendChild(executeButton);
|
414 |
|
|
|
416 |
}
|
417 |
|
418 |
/**
|
419 |
+
* ?�로그램 ?�행 ?�수
|
420 |
+
* @param {string} programId - ?�행???�로그램 ID
|
421 |
+
* @param {string} programName - ?�로그램 ?�름 (?�림??
|
422 |
*/
|
423 |
async function executeProgram(programId, programName) {
|
424 |
if (!programId) return;
|
425 |
|
426 |
+
console.log(`?�로그램 ?�행 ?�청: ${programId} (${programName})`);
|
427 |
|
428 |
+
// ?�행 ?�인
|
429 |
+
if (!confirm(`'${programName}' ?�로그램???�행?�시겠습?�까?`)) {
|
430 |
+
console.log('?�로그램 ?�행 취소??);
|
431 |
return;
|
432 |
}
|
433 |
|
434 |
try {
|
435 |
+
// 로딩 ?�림 ?�시
|
436 |
+
showNotification(`'${programName}' ?�행 �?..`, 'info');
|
437 |
|
438 |
+
// API ?�청 - ?�정??URL 구성 ?�용
|
439 |
const response = await fetch(getDeviceApiUrl(`/api/programs/${programId}/execute`), {
|
440 |
method: 'POST',
|
441 |
headers: {
|
442 |
'Content-Type': 'application/json'
|
443 |
},
|
444 |
+
body: JSON.stringify({}) // ?�요 ??추�? ?�라미터 ?�달
|
445 |
});
|
446 |
|
447 |
+
// ?�답 처리
|
448 |
if (!response.ok) {
|
449 |
+
throw new Error(`HTTP ?�류: ${response.status}`);
|
450 |
}
|
451 |
|
452 |
const data = await response.json();
|
453 |
+
console.log(`?�로그램 ?�행 ?�답:`, data);
|
454 |
|
455 |
// 결과 처리
|
456 |
if (data.success) {
|
457 |
+
showNotification(`'${programName}' ?�행 ?�공: ${data.message}`, 'success');
|
458 |
} else {
|
459 |
+
showNotification(`'${programName}' ?�행 ?�패: ${data.message || '?????�는 ?�류'}`, 'error');
|
460 |
}
|
461 |
} catch (error) {
|
462 |
+
console.error(`?�로그램 ?�행 ?�류 (${programId}):`, error);
|
463 |
+
showNotification(`'${programName}' ?�행 ?�류: ${handleError(error)}`, 'error');
|
464 |
}
|
465 |
}
|
466 |
|
467 |
/**
|
468 |
+
* ?�림 ?�시 ?�수
|
469 |
+
* @param {string} message - ?�림 메시지
|
470 |
+
* @param {string} type - ?�림 ?�형 ('success', 'error', 'warning')
|
471 |
*/
|
472 |
function showNotification(message, type = 'info') {
|
473 |
+
// 기존 ?�림???�으�??�거
|
474 |
const existingNotification = document.querySelector('.notification');
|
475 |
if (existingNotification) {
|
476 |
existingNotification.remove();
|
477 |
}
|
478 |
|
479 |
+
// ???�림 ?�성
|
480 |
const notification = document.createElement('div');
|
481 |
notification.classList.add('notification', type);
|
482 |
notification.textContent = message;
|
483 |
|
484 |
+
// ?�림 ?�기 버튼
|
485 |
const closeButton = document.createElement('button');
|
486 |
closeButton.classList.add('notification-close');
|
487 |
closeButton.innerHTML = '×';
|
|
|
491 |
|
492 |
notification.appendChild(closeButton);
|
493 |
|
494 |
+
// 문서???�림 추�?
|
495 |
document.body.appendChild(notification);
|
496 |
|
497 |
+
// ?�정 ?�간 ???�동?�로 ?�라지?�록 ?�정
|
498 |
setTimeout(() => {
|
499 |
if (document.body.contains(notification)) {
|
500 |
notification.remove();
|
501 |
}
|
502 |
+
}, 5000); // 5�????�라�?}
|
|
|
503 |
|
504 |
+
// checkDeviceStatus ?�수�??�역?�로 ?�출
|
505 |
+
window.checkDeviceStatus = checkDeviceStatus;
|