jeongsoo commited on
Commit
1d4fc8e
·
1 Parent(s): 2883051
Files changed (2) hide show
  1. app/app_routes.py +176 -191
  2. app/static/js/app-device.js +148 -160
app/app_routes.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- RAG 검색 챗봇 웹 애플리케이션 - API 라우트 정의
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"-------------- 로그인 페이지 접속 (Next: {next_url}) --------------")
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"입력된 사용자명: {username}")
43
- logger.info(f"비밀번호 입력 여부: {len(password) > 0}")
44
 
45
- # 환경 변수 또는 기본값과 비교
46
  valid_username = ADMIN_USERNAME
47
  valid_password = ADMIN_PASSWORD
48
- logger.info(f"검증용 사용자명: {valid_username}")
49
- logger.info(f"검증용 비밀번호 존재 여부: {valid_password is not None and len(valid_password) > 0}")
50
 
51
  if username == valid_username and password == valid_password:
52
- logger.info(f"로그인 성공: {username}")
53
- # 세션 설정 현재 세션 상태 로깅
54
- logger.debug(f"세션 설정 전: {session}")
55
 
56
- # 세션에 로그인 정보 저장
57
- session.permanent = True
58
  session['logged_in'] = True
59
  session['username'] = username
60
  session.modified = True
61
 
62
- logger.info(f"세션 설정 후: {session}")
63
- logger.info("세션 설정 완료, 리디렉션 시도")
64
 
65
- # 로그인 성공 후 리디렉션
66
  redirect_to = next_url or url_for('index')
67
- logger.info(f"리디렉션 대상: {redirect_to}")
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("로그인 페이지 GET 요청")
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"사용자 {username} 로그아웃 처리 시작")
91
  session.pop('logged_in', None)
92
  session.pop('username', None)
93
  session.modified = True
94
- logger.info(f"세션 정보 삭제 완료. 현재 세션: {session}")
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
- # 준비 상태 확인 - 30 이상 지났으면 강제로 ready 상태로 변경
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"앱이 30 이상 초기화 중 상태입니다. 강제로 ready 상태로 변경합니다.")
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
- """ 초기화 상태 확인 API"""
130
- logger.info(f" 상태 확인 요청: {'Ready' if app_ready else 'Not Ready'}")
131
  return jsonify({"ready": app_ready})
132
 
133
 
134
  @app.route('/api/llm', methods=['GET', 'POST'])
135
  @login_required
136
  def llm_api():
137
- """사용 가능한 LLM 목록 선택 API"""
138
  if not app_ready:
139
- return jsonify({"error": "앱이 아직 초기화 중입니다. 잠시 후 다시 시도해주세요."}), 503
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 정보 조회 오류: {e}")
156
- return jsonify({"error": "LLM 정보 조회 오류 발생"}), 500
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가 제공되지 않았습니다."}), 400
162
 
163
  llm_id = data['llm_id']
164
- logger.info(f"LLM 변경 요청: {llm_id}")
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"지원되지 않는 LLM ID: {llm_id}"}), 400
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'{new_details.get('name', llm_id)}'로 변경되었습니다.")
177
  return jsonify({
178
  "success": True,
179
- "message": f"LLM'{new_details.get('name', llm_id)}'로 변경되었습니다.",
180
  "current_llm": new_details
181
  })
182
  else:
183
- logger.error(f"LLM 변경 실패 (ID: {llm_id})")
184
- return jsonify({"error": "LLM 변경 내부 오류 발생"}), 500
185
  except Exception as e:
186
- logger.error(f"LLM 변경 처리 중 오류: {e}", exc_info=True)
187
- return jsonify({"error": f"LLM 변경 중 오류 발생: {str(e)}"}), 500
188
 
189
 
190
  @app.route('/api/chat', methods=['POST'])
191
  @login_required
192
  def chat():
193
- """텍스트 기반 챗봇 API"""
194
  if not app_ready or retriever is None:
195
- return jsonify({"error": "앱/검색기가 아직 초기화 중입니다. 잠시 후 다시 시도해주세요."}), 503
196
 
197
  try:
198
  data = request.get_json()
199
  if not data or 'query' not in data:
200
- return jsonify({"error": "쿼리가 제공되지 않았습니다."}), 400
201
 
202
  query = data['query']
203
- logger.info(f"텍스트 쿼리 수신: {query[:100]}...")
204
 
205
- # RAG 검색 수행
206
  if not hasattr(retriever, 'search'):
207
- raise NotImplementedError("Retrieversearch 메소드가 없습니다.")
208
  search_results = retriever.search(query, top_k=5, first_stage_k=6)
209
 
210
- # 컨텍스트 준비
211
- if not hasattr(DocumentProcessor, 'prepare_rag_context'):
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("LLMInterfacerag_generate 메소드가 없습니다.")
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 응답 생성 완료 (길이: {len(answer)})")
229
 
230
- # 소스 정보 추출 (CSV ID 추출 로직 포함)
231
  sources = []
232
  if search_results:
233
  for result in search_results:
234
  if not isinstance(result, dict):
235
- logger.warning(f"예상치 못한 검색 결과 형식: {type(result)}")
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 소스 ID 추출: {first_column} from {source_info['source']}")
254
  except Exception as e:
255
- logger.warning(f"CSV 소스 ID 추출 실패 ({result.get('source')}): {e}")
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"채팅 처리 오류 발생: {e}", exc_info=True)
269
- return jsonify({"error": f"처리 오류가 발생했습니다: {str(e)}"}), 500
270
 
271
 
272
  @app.route('/api/voice', methods=['POST'])
273
  @login_required
274
  def voice_chat():
275
- """음성 API 엔드포인트"""
276
  if not app_ready:
277
- logger.warning(" 초기화가 완료되지 않았지만 음성 API 요청 처리 시도")
278
- # 여기서 바로 리턴하지 않고 계속 진행
279
- # 사전 검사: retriever stt_client가 제대로 초기화되었는지 확인
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": "오디오 파일이 제공되지 않았습니다."}), 400
301
 
302
  audio_file = request.files['audio']
303
- logger.info(f"수신된 오디오 파일: {audio_file.filename} ({audio_file.content_type})")
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"오디오 파일을 임시 저장: {temp_audio.name}")
311
- # VitoSTT.transcribe_audio 가 파일 경로 또는 바이트를 받을 있도록 구현되어야
312
- # 여기서는 파일 경로를 사용한다고 가정
313
- if not hasattr(stt_client, 'transcribe_audio'):
314
- raise NotImplementedError("STT 클라이언트에 transcribe_audio 메소드가 없습니다.")
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", " 수 없는 STT 오류") if isinstance(stt_result, dict) else "STT 결과 형식 오류"
326
- logger.error(f"음성인식 실패: {error_msg}")
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": "음성에서 텍스트를 인식하지 못했습니다.", "transcription": ""}), 400
336
 
337
- logger.info(f"음성인식 성공: {transcription[:50]}...")
338
  if retriever is None:
339
- logger.error("STT 성공 검색 시도 중 retriever가 None임")
340
  return jsonify({
341
  "transcription": transcription,
342
- "answer": "음성을 인식했지만, 현재 검색 시스템이 준비되지 않았습니다. 잠시 다시 시도해주세요.",
343
  "sources": []
344
  })
345
- # --- 이후 로직은 /api/chat 거의 동일 ---
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 = "죄송합니다. 관련 정보를 찾을 수 없습니다." (아래 LLM 호출 로직에서 처���)
353
  pass
354
 
355
- # LLM 호출
356
- llm_id = request.form.get('llm_id', None) # 음성 요청은 form 데이터로 LLM ID 받을 수 있음
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 응답 생성 완료 (길이: {len(answer)})")
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"[음성챗] CSV 소스 ID 추출 실패 ({doc.get('source')}): {e}")
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"음성 처리 중 오류 발생: {e}", exc_info=True)
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
- """지식베이스 문서 업로드 API"""
408
- if not app_ready or base_retriever is None:
409
- return jsonify({"error": "앱/기본 검색기가 아직 초기화 중입니다."}), 503
410
 
411
  if 'document' not in request.files:
412
- return jsonify({"error": "문서 파일이 제공되지 않았습니다."}), 400
413
 
414
  doc_file = request.files['document']
415
  if doc_file.filename == '':
416
- return jsonify({"error": "선택된 파일이 없습니다."}), 400
417
 
418
  if not allowed_doc_file(doc_file.filename):
419
- logger.error(f"허용되지 않는 파일 형식: {doc_file.filename}")
420
- return jsonify({"error": f"허용되지 않는 파일 형식입니다. 허용: {', '.join(ALLOWED_DOC_EXTENSIONS)}"}), 400
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"문서 저장 완료: {filepath}")
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 디코딩 실패, CP949 시도: {filename}")
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 디코딩 실패 ({filename}): {e_cp949}")
439
- return jsonify({"error": "파일 인코딩을 읽을 없습니다 (UTF-8, CP949 시도 실패)."}), 400
440
  except Exception as e_read:
441
- logger.error(f"파일 읽기 오류 ({filename}): {e_read}")
442
- return jsonify({"error": f"파일 읽기 오류 발생: {str(e_read)}"}), 500
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 파일 처리 시작: {filename}")
459
- docs = DocumentProcessor.csv_to_documents(content, metadata) # 단위 처리 가정
460
- else: # 기타 텍스트 기반 문서
461
- logger.info(f"일반 텍스트 문서 처리 시작: {filename}")
462
- # PDF, DOCX 등은 별도 라이브러리(pypdf, python-docx) 필요
463
  if file_ext in ['pdf', 'docx']:
464
- logger.warning(f".{file_ext} 파일 처리는 현재 구현되지 않았습니다. 텍스트 추출 로직 추가 필요.")
465
- # 여기에 pdf/docx 텍스트 추출 로직 추가
466
- # 예: content = extract_text_from_pdf(filepath)
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("기본 검색기에 add_documents 또는 save 메소드 없음")
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
- # 예: retriever.update_base_retriever(base_retriever)
493
  return jsonify({
494
  "success": True,
495
- "message": f"파일 '{filename}' 업로드 처리 완료 ({len(docs)} 청크 추가)."
496
  })
497
  except Exception as e_save:
498
- logger.error(f"인덱스 저장 중 오류 발생: {e_save}")
499
- return jsonify({"error": f"인덱스 저장 중 오류: {str(e_save)}"}), 500
500
  else:
501
- logger.warning(f"파일 '{filename}'에서 처리할 내용이 없거나 지원되지 않는 형식입니다.")
502
- # 파일은 저장되었으므로 성공으로 간주할지 결정 필요
503
  return jsonify({
504
  "warning": True,
505
- "message": f"파일 '{filename}'이 저장되었지만 처리할 내용이 없습니다."
506
  })
507
 
508
  except Exception as e:
509
- logger.error(f"파일 업로드 또는 처리 오류 발생: {e}", exc_info=True)
510
- return jsonify({"error": f"파일 업로드 중 오류: {str(e)}"}), 500
511
 
512
 
513
  @app.route('/api/documents', methods=['GET'])
514
  @login_required
515
  def list_documents():
516
- """지식베이스 문서 목록 API"""
517
- if not app_ready or base_retriever is None:
518
- return jsonify({"error": "앱/기본 검색기가 아직 초기화 중입니다."}), 503
519
 
520
  try:
521
  sources = {}
522
  total_chunks = 0
523
- # base_retriever.documents 같은 속성이 실제 클래스에 있다고 가정
524
- if hasattr(base_retriever, 'documents') and base_retriever.documents:
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") # 메타데이터에서 source 가져오기
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("검색기에 문서가 없거나 documents 속성을 찾을 수 없습니다.")
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"문서 목록 조회 완료: {len(documents)} 소스 파일, {total_chunks}개 청크")
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"문서 목록 조회 오류 발생: {e}", exc_info=True)
567
- return jsonify({"error": f"문서 목록 조회 오류: {str(e)}"}), 500
 
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 검색 챗봇 장치 관리 JavaScript
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
- // 장치 서버 URL 설정
23
- // 직접 접근 설정: 문자열이면 현재 호스트의 5051 포트를 사용
24
- let DEVICE_SERVER_URL = ''; // 서버에서 전달된 URL이 있으면 초기화 시 설정됨
25
-
26
  /**
27
- * 장치 서버 API 경로 생성 함수
28
- * @param {string} endpoint - API 엔드포인트 경로
29
- * @returns {string} - 완전한 API URL
30
  */
31
  function getDeviceApiUrl(endpoint) {
32
- // 직접 접근 모드인 경우 (별도 서버에 직접 요청)
33
  if (DEVICE_SERVER_URL) {
34
  return `${DEVICE_SERVER_URL}${endpoint}`;
35
  }
36
 
37
- // 프록시 모드인 경우 (내부 API 경로 사용)
38
  return endpoint;
39
  }
40
 
41
  /**
42
- * 장치 서버 설정 초기화 (페이지 로드 시 호출)
43
  */
44
  function initDeviceServerSettings() {
45
- console.log("장치 서버 설정 초기화 시작");
46
- // 서버 URL 설정 ( 서버로부터 설정 불러오기)
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(`장치 서버 URL 설정됨: ${data.server_url}`);
57
  DEVICE_SERVER_URL = data.server_url;
58
  } else {
59
- // 설정 없음 - 자동 생성 (현재 호스트 + 포트 5051)
60
  const currentHost = window.location.hostname;
61
  const protocol = window.location.protocol;
62
  DEVICE_SERVER_URL = `${protocol}//${currentHost}:5051`;
63
- console.log(`장치 서버 URL 자동 설정: ${DEVICE_SERVER_URL}`);
64
  }
65
  })
66
  .catch(error => {
67
- console.error('장치 서버 설정 초기화 오류:', error);
68
- // 기본값으로 설정 (현재 호스트 + 포트 5051)
69
  const currentHost = window.location.hostname;
70
  const protocol = window.location.protocol;
71
  DEVICE_SERVER_URL = `${protocol}//${currentHost}:5051`;
72
- console.log(`장치 서버 URL 기본값 설정: ${DEVICE_SERVER_URL}`);
73
  });
74
  }
75
 
76
- // 페이지 로드 초기화
77
- document.addEventListener('DOMContentLoaded', () => {
78
- console.log("장치 관리 모듈 초기화");
79
 
80
- // 장치 서버 설정 초기화
81
- initDeviceServerSettings();
82
 
83
- // 전환 이벤트 리스너는 이미 app.js에서 등록되어 있으므로 여기서는 등록하지 않음
84
- // 대신 전역 함수가 올바르게 설정되어 있는지 확인
85
  if (typeof window.switchTab !== 'function') {
86
- console.log("window.switchTab 함수가 정의되지 않았습니다. 내부 구현을 사용합니다.");
87
  } else {
88
- console.log("window.switchTab 함수 사용 가능합니다.");
89
  }
90
 
91
- // 장치 상태 확인 버튼 이벤트 리스너
92
- checkDeviceStatusButton.addEventListener('click', () => {
93
- console.log("장치 상태 확인 버튼 클릭");
94
  checkDeviceStatus();
95
  });
96
 
97
- // 장치 목록 새로고침 버튼 이벤트 리스너
98
- refreshDevicesButton.addEventListener('click', () => {
99
- console.log("장치 목록 새로고침 버튼 클릭");
100
  loadDevices();
101
  });
102
 
103
- // 프로그램 목록 로드 버튼 이벤트 리스너
104
- loadProgramsButton.addEventListener('click', () => {
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("오류 발생:", 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 `오류가 발생했습니다: ${error.message || ' 수 없는 오류'}`;
127
  }
128
 
129
  /**
130
- * HTML 이스케이프 함수 (XSS 방지)
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
- * 장치 관리 서버 상태 확인 함수 - 전역 함수로 export
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 오류: ${response.status}`);
169
  }
170
 
171
  const data = await response.json();
172
- console.log("장치 상태 응답:", data);
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 = `서버 상태: ${data.status || '정상'}`;
182
 
183
- // 자동으로 장치 목록 로드
184
  loadDevices();
185
  } else {
186
- // 오프라인 또는 오류 상태인 경우
187
  statusIcon.innerHTML = '<i class="fas fa-circle offline"></i>';
188
- statusText.textContent = `서버 오류: ${data.error || ' 수 없는 오류'}`;
189
  }
190
  } catch (error) {
191
- console.error("장치 상태 확인 오류:", 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 오류: ${response.status}`);
228
  }
229
 
230
  const data = await response.json();
231
- console.log("장치 목록 응답:", data);
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("장치 목록 로드 오류:", 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} - 장치 아이템 DOM 요소
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 = `유형: ${device.type || '알 수 없음'}`;
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 ? ', ' : '') + `마지막 활동: ${device.lastSeen}`;
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 오류: ${response.status}`);
359
  }
360
 
361
  const data = await response.json();
362
- console.log("프로그램 목록 응답:", data);
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("프로그램 목록 로드 오류:", 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} - 프로그램 아이템 DOM 요소
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 - 실행할 프로그램 ID
432
- * @param {string} programName - 프로그램 이름 (알림용)
433
  */
434
  async function executeProgram(programId, programName) {
435
  if (!programId) return;
436
 
437
- console.log(`프로그램 실행 요청: ${programId} (${programName})`);
438
 
439
- // 실행 확인
440
- if (!confirm(`'${programName}' 프로그램을 실행하시겠습니까?`)) {
441
- console.log('프로그램 실행 취소됨');
442
  return;
443
  }
444
 
445
  try {
446
- // 로딩 알림 표시
447
- showNotification(`'${programName}' 실행 중...`, 'info');
448
 
449
- // API 요청 - 수정된 URL 구성 사용
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 오류: ${response.status}`);
461
  }
462
 
463
  const data = await response.json();
464
- console.log(`프로그램 실행 응답:`, data);
465
 
466
  // 결과 처리
467
  if (data.success) {
468
- showNotification(`'${programName}' 실행 성공: ${data.message}`, 'success');
469
  } else {
470
- showNotification(`'${programName}' 실행 실패: ${data.message || ' 수 없는 오류'}`, 'error');
471
  }
472
  } catch (error) {
473
- console.error(`프로그램 실행 오류 (${programId}):`, error);
474
- showNotification(`'${programName}' 실행 오류: ${handleError(error)}`, 'error');
475
  }
476
  }
477
 
478
  /**
479
- * 알림 표시 함수
480
- * @param {string} message - 알림 메시지
481
- * @param {string} type - 알림 유형 ('success', 'error', 'warning')
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 = '&times;';
@@ -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 = '&times;';
 
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;