jeongsoo commited on
Commit
12b4400
ยท
1 Parent(s): 460bd69
Files changed (3) hide show
  1. app/app.py +826 -18
  2. app/static/js/app.js +621 -43
  3. app/templates/index.html +0 -47
app/app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์žฅ์น˜ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ํ†ตํ•ฉ)
3
  """
4
 
5
  import os
@@ -7,19 +7,16 @@ import json
7
  import logging
8
  import tempfile
9
  import threading
10
- import requests
11
- from datetime import datetime, timedelta
12
  from flask import Flask, request, jsonify, render_template, send_from_directory, session, redirect, url_for
13
  from werkzeug.utils import secure_filename
14
  from dotenv import load_dotenv
15
  from functools import wraps
16
- import pickle
17
- import gzip
18
 
19
  # ๋กœ๊ฑฐ ์„ค์ •
20
  logging.basicConfig(
21
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
22
- level=logging.DEBUG
23
  )
24
  logger = logging.getLogger(__name__)
25
 
@@ -29,13 +26,11 @@ load_dotenv()
29
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊น…
30
  ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
31
  ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
32
- DEVICE_SERVER_URL = os.getenv('DEVICE_SERVER_URL', 'http://localhost:5050')
33
 
34
  logger.info(f"==== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ====")
35
  logger.info(f"ADMIN_USERNAME ์„ค์ • ์—ฌ๋ถ€: {ADMIN_USERNAME is not None}")
36
  # ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋กœ๋“œ ์—ฌ๋ถ€๋งŒ ๊ธฐ๋ก (๋ณด์•ˆ)
37
  logger.info(f"ADMIN_PASSWORD ์„ค์ • ์—ฌ๋ถ€: {ADMIN_PASSWORD is not None}")
38
- logger.info(f"DEVICE_SERVER_URL: {DEVICE_SERVER_URL}")
39
 
40
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (๊ฐœ๋ฐœ์šฉ, ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ถŒ์žฅ)
41
  if not ADMIN_USERNAME:
@@ -45,10 +40,10 @@ if not ADMIN_USERNAME:
45
  if not ADMIN_PASSWORD:
46
  ADMIN_PASSWORD = 'rag12345'
47
  logger.warning("ADMIN_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'rag12345'๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
48
-
49
  class MockComponent: pass
50
 
51
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
 
52
  try:
53
  from utils.vito_stt import VitoSTT
54
  from utils.llm_interface import LLMInterface
@@ -57,6 +52,8 @@ try:
57
  from retrieval.reranker import ReRanker
58
  except ImportError as e:
59
  logger.error(f"๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}. utils ๋ฐ retrieval ํŒจํ‚ค์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒฝ๋กœ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
 
 
60
  VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
61
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ๋ ---
62
 
@@ -65,15 +62,20 @@ except ImportError as e:
65
  app = Flask(__name__)
66
 
67
  # ์„ธ์…˜ ์„ค์ • - ๊ณ ์ •๋œ ์‹œํฌ๋ฆฟ ํ‚ค ์‚ฌ์šฉ (์‹ค์ œ ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ์œผ๋กœ ๊ด€๋ฆฌ ๊ถŒ์žฅ)
68
- app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345')
69
 
70
- # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ---
 
71
  app.config['SESSION_COOKIE_SECURE'] = True
72
- app.config['SESSION_COOKIE_HTTPONLY'] = True
73
- app.config['SESSION_COOKIE_SAMESITE'] = 'None'
74
- app.config['SESSION_COOKIE_DOMAIN'] = None
75
- app.config['SESSION_COOKIE_PATH'] = '/'
76
- app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=1)
 
 
 
 
77
  # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ๋ ---
78
 
79
  # ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์„ค์ • (10MB)
@@ -108,21 +110,827 @@ app_ready = False # ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
108
  # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋ ---
109
 
110
 
111
- # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ---
112
  def login_required(f):
113
  @wraps(f)
114
  def decorated_function(*args, **kwargs):
115
  logger.info(f"----------- ์ธ์ฆ ํ•„์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ๋„: {request.path} -----------")
116
  logger.info(f"ํ˜„์žฌ ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜ ๊ฐ์ฒด: {session}")
117
  logger.info(f"ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
 
118
  logger.info(f"์š”์ฒญ์˜ ์„ธ์…˜ ์ฟ ํ‚ค ๊ฐ’: {request.cookies.get('session', 'None')}")
119
 
120
  # Flask ์„ธ์…˜์— 'logged_in' ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ์ง์ ‘ ํ™•์ธ
121
  if 'logged_in' not in session:
122
  logger.warning(f"ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜์— 'logged_in' ์—†์Œ. ๋กœ๊ทธ์ธ ํŽ˜๏ฟฝ๏ฟฝ๏ฟฝ์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜.")
123
- return redirect(url_for('login', next=request.url))
 
124
 
125
  logger.info(f"์ธ์ฆ ์„ฑ๊ณต: {session.get('username', 'unknown')} ์‚ฌ์šฉ์ž๊ฐ€ {request.path} ์ ‘๊ทผ")
126
  return f(*args, **kwargs)
127
  return decorated_function
128
  # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์„ธ์…˜ ์„ค์ • ์ˆ˜์ • ์ ์šฉ)
3
  """
4
 
5
  import os
 
7
  import logging
8
  import tempfile
9
  import threading
10
+ import datetime
 
11
  from flask import Flask, request, jsonify, render_template, send_from_directory, session, redirect, url_for
12
  from werkzeug.utils import secure_filename
13
  from dotenv import load_dotenv
14
  from functools import wraps
 
 
15
 
16
  # ๋กœ๊ฑฐ ์„ค์ •
17
  logging.basicConfig(
18
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
+ level=logging.DEBUG # INFO์—์„œ DEBUG๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋” ์ƒ์„ธํ•œ ๋กœ๊ทธ ํ™•์ธ
20
  )
21
  logger = logging.getLogger(__name__)
22
 
 
26
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊น…
27
  ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
28
  ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
 
29
 
30
  logger.info(f"==== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ====")
31
  logger.info(f"ADMIN_USERNAME ์„ค์ • ์—ฌ๋ถ€: {ADMIN_USERNAME is not None}")
32
  # ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋กœ๋“œ ์—ฌ๋ถ€๋งŒ ๊ธฐ๋ก (๋ณด์•ˆ)
33
  logger.info(f"ADMIN_PASSWORD ์„ค์ • ์—ฌ๋ถ€: {ADMIN_PASSWORD is not None}")
 
34
 
35
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (๊ฐœ๋ฐœ์šฉ, ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ถŒ์žฅ)
36
  if not ADMIN_USERNAME:
 
40
  if not ADMIN_PASSWORD:
41
  ADMIN_PASSWORD = 'rag12345'
42
  logger.warning("ADMIN_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'rag12345'๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
 
43
  class MockComponent: pass
44
 
45
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
46
+ # ์‹ค์ œ ๊ฒฝ๋กœ์— ๋งž๊ฒŒ utils, retrieval ํด๋”๊ฐ€ ์กด์žฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
47
  try:
48
  from utils.vito_stt import VitoSTT
49
  from utils.llm_interface import LLMInterface
 
52
  from retrieval.reranker import ReRanker
53
  except ImportError as e:
54
  logger.error(f"๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}. utils ๋ฐ retrieval ํŒจํ‚ค์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒฝ๋กœ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
55
+ # ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ž„์‹œ ํด๋ž˜์Šค ์ •์˜ (์‹ค์ œ ์‚ฌ์šฉ ์‹œ ์ œ๊ฑฐ)
56
+
57
  VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
58
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ๋ ---
59
 
 
62
  app = Flask(__name__)
63
 
64
  # ์„ธ์…˜ ์„ค์ • - ๊ณ ์ •๋œ ์‹œํฌ๋ฆฟ ํ‚ค ์‚ฌ์šฉ (์‹ค์ œ ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ์œผ๋กœ ๊ด€๋ฆฌ ๊ถŒ์žฅ)
65
+ app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345') # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์šฐ์„  ์‚ฌ์šฉ
66
 
67
+ # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ์ˆ˜์ • (ํ—ˆ๊น…ํŽ˜์ด์Šค ํ™˜๊ฒฝ ๊ณ ๋ ค) ---
68
+ # ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ HTTPS๋กœ ์„œ๋น„์Šค๋˜๋ฏ€๋กœ Secure=True ์„ค์ •
69
  app.config['SESSION_COOKIE_SECURE'] = True
70
+ app.config['SESSION_COOKIE_HTTPONLY'] = True # JavaScript์—์„œ ์ฟ ํ‚ค ์ ‘๊ทผ ๋ฐฉ์ง€ (๋ณด์•ˆ ๊ฐ•ํ™”)
71
+ # SameSite='Lax'๊ฐ€ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ์— ๋” ์•ˆ์ „ํ•˜๊ณ  ํ˜ธํ™˜์„ฑ์ด ์ข‹์Œ.
72
+ # ๋งŒ์•ฝ ์•ฑ์ด ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ iframe ๋‚ด์—์„œ ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค๋ฉด 'None'์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•จ.
73
+ # (๋‹จ, 'None'์œผ๋กœ ์„ค์ • ์‹œ ๋ฐ˜๋“œ์‹œ Secure=True์—ฌ์•ผ ํ•จ)
74
+ # ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ iframe ํ™˜๊ฒฝ์œผ๋กœ ํ™•์ธ๋˜์–ด 'None'์œผ๋กœ ๋ณ€๊ฒฝ
75
+ app.config['SESSION_COOKIE_SAMESITE'] = 'None' # <--- ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
76
+ app.config['SESSION_COOKIE_DOMAIN'] = None # ํŠน์ • ๋„๋ฉ”์ธ ์ œํ•œ ์—†์Œ
77
+ app.config['SESSION_COOKIE_PATH'] = '/' # ์•ฑ ์ „์ฒด ๊ฒฝ๋กœ์— ์ฟ ํ‚ค ์ ์šฉ
78
+ app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=1) # ์„ธ์…˜ ์œ ํšจ ์‹œ๊ฐ„ ์ฆ๊ฐ€
79
  # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ๋ ---
80
 
81
  # ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์„ค์ • (10MB)
 
110
  # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋ ---
111
 
112
 
113
+ # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ (์ˆ˜์ •๋จ) ---
114
  def login_required(f):
115
  @wraps(f)
116
  def decorated_function(*args, **kwargs):
117
  logger.info(f"----------- ์ธ์ฆ ํ•„์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ๋„: {request.path} -----------")
118
  logger.info(f"ํ˜„์žฌ ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜ ๊ฐ์ฒด: {session}")
119
  logger.info(f"ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
120
+ # ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ณด๋‚ธ ์‹ค์ œ ์ฟ ํ‚ค ํ™•์ธ (๋””๋ฒ„๊น…์šฉ)
121
  logger.info(f"์š”์ฒญ์˜ ์„ธ์…˜ ์ฟ ํ‚ค ๊ฐ’: {request.cookies.get('session', 'None')}")
122
 
123
  # Flask ์„ธ์…˜์— 'logged_in' ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ์ง์ ‘ ํ™•์ธ
124
  if 'logged_in' not in session:
125
  logger.warning(f"ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜์— 'logged_in' ์—†์Œ. ๋กœ๊ทธ์ธ ํŽ˜๏ฟฝ๏ฟฝ๏ฟฝ์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜.")
126
+ # ์ˆ˜๋™ ์ฟ ํ‚ค ํ™•์ธ ๋กœ์ง ์ œ๊ฑฐ๋จ
127
+ return redirect(url_for('login', next=request.url)) # ๋กœ๊ทธ์ธ ํ›„ ์›๋ž˜ ํŽ˜์ด์ง€๋กœ ๋Œ์•„๊ฐ€๋„๋ก next ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
128
 
129
  logger.info(f"์ธ์ฆ ์„ฑ๊ณต: {session.get('username', 'unknown')} ์‚ฌ์šฉ์ž๊ฐ€ {request.path} ์ ‘๊ทผ")
130
  return f(*args, **kwargs)
131
  return decorated_function
132
  # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ ---
133
+
134
+
135
+ # --- ํ—ฌํผ ํ•จ์ˆ˜ ---
136
+ def allowed_audio_file(filename):
137
+ """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ์˜ค๋””์˜ค ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
138
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
139
+
140
+ def allowed_doc_file(filename):
141
+ """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ๋ฌธ์„œ ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
142
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
143
+ # --- ํ—ฌํผ ํ•จ์ˆ˜ ๋ ---
144
+
145
+
146
+ # init_retriever ํ•จ์ˆ˜ ๋‚ด๋ถ€์— ๋กœ๊น… ์ถ”๊ฐ€ ์˜ˆ์‹œ
147
+ # --- ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ๊ด€๋ จ ํ•จ์ˆ˜ ---
148
+ def init_retriever():
149
+ """๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋กœ๋“œ"""
150
+ global base_retriever, retriever
151
+
152
+ index_path = app.config['INDEX_PATH']
153
+ data_path = app.config['DATA_FOLDER'] # data_path ์ •์˜ ํ™•์ธ
154
+ logger.info("--- init_retriever ์‹œ์ž‘ ---")
155
+
156
+ # 1. ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™”
157
+ # ... (VectorRetriever ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™” ๋กœ์ง์€ ์ด์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€) ...
158
+ # VectorRetriever ์ดˆ๊ธฐํ™”/๋กœ๋“œ ์‹คํŒจ ์‹œ base_retriever = None ๋ฐ return None ์ฒ˜๋ฆฌ ํฌํ•จ
159
+ if os.path.exists(os.path.join(index_path, "documents.json")):
160
+ try:
161
+ logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹œ๋„: {index_path}")
162
+ base_retriever = VectorRetriever.load(index_path)
163
+ logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์„ฑ๊ณต. ๋ฌธ์„œ {len(getattr(base_retriever, 'documents', []))}๊ฐœ")
164
+ except Exception as e:
165
+ logger.error(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹คํŒจ: {e}", exc_info=True)
166
+ logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
167
+ try:
168
+ base_retriever = VectorRetriever()
169
+ logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
170
+ except Exception as e_init:
171
+ logger.error(f"์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_init}", exc_info=True)
172
+ base_retriever = None
173
+ else:
174
+ logger.info("์ธ๋ฑ์Šค ํŒŒ์ผ ์—†์Œ. ์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
175
+ try:
176
+ base_retriever = VectorRetriever()
177
+ logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
178
+ except Exception as e_init:
179
+ logger.error(f"์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_init}", exc_info=True)
180
+ base_retriever = None
181
+
182
+ if base_retriever is None:
183
+ logger.error("base_retriever ์ดˆ๊ธฐํ™”/๋กœ๋“œ์— ์‹คํŒจํ•˜์—ฌ init_retriever ์ค‘๋‹จ.")
184
+ return None
185
+
186
+ # 2. ๋ฐ์ดํ„ฐ ํด๋” ๋ฌธ์„œ ๋กœ๋“œ (๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ)
187
+ needs_loading = (not hasattr(base_retriever, 'documents') or not getattr(base_retriever, 'documents', None)) # None ์ฒดํฌ ์ถ”๊ฐ€
188
+ if needs_loading and os.path.exists(data_path):
189
+ logger.info(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์–ด {data_path}์—์„œ ๋ฌธ์„œ ๋กœ๋“œ ์‹œ๋„...")
190
+ try:
191
+ # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 1 ์‹œ์ž‘ ==================
192
+ # DocumentProcessor.load_documents_from_directory ํ˜ธ์ถœ ์‹œ ์˜ฌ๋ฐ”๋ฅธ ์ธ์ž ์ „๋‹ฌ
193
+ docs = DocumentProcessor.load_documents_from_directory(
194
+ directory=data_path, # <-- ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ์‚ฌ์šฉ
195
+ extensions=[".txt", ".md", ".csv"], # <-- ํ•„์š”ํ•œ ํ™•์žฅ์ž ์ „๋‹ฌ
196
+ recursive=True # <-- ์žฌ๊ท€ ํƒ์ƒ‰ ์—ฌ๋ถ€ ์ „๋‹ฌ
197
+ )
198
+ # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 1 ๋ ====================
199
+ logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ๋กœ๋“œ ์„ฑ๊ณต.")
200
+ if docs and hasattr(base_retriever, 'add_documents'):
201
+ logger.info("๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ ์ถ”๊ฐ€ ์‹œ๋„...")
202
+ base_retriever.add_documents(docs)
203
+ logger.info("๋ฌธ์„œ ์ถ”๊ฐ€ ์™„๋ฃŒ.")
204
+
205
+ if hasattr(base_retriever, 'save'):
206
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ ์ €์žฅ ์‹œ๋„: {index_path}")
207
+ try:
208
+ base_retriever.save(index_path)
209
+ logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ.")
210
+ except Exception as e_save:
211
+ logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์‹คํŒจ: {e_save}", exc_info=True)
212
+ except Exception as e_load_add:
213
+ # load_documents_from_directory ์ž์ฒด์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋‚  ์ˆ˜๋„ ์žˆ์Œ (๊ถŒํ•œ ๋“ฑ)
214
+ logger.error(f"DATA_FOLDER ๋ฌธ์„œ ๋กœ๋“œ/์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜: {e_load_add}", exc_info=True)
215
+
216
+ # 3. ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
217
+ logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
218
+ try:
219
+ # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 2 ์‹œ์ž‘ ==================
220
+ # custom_rerank_fn ํ•จ์ˆ˜๋ฅผ ReRanker ์ดˆ๊ธฐ๏ฟฝ๏ฟฝ ์ „์— ์ •์˜
221
+ def custom_rerank_fn(query, results):
222
+ query_terms = set(query.lower().split())
223
+ for result in results:
224
+ if isinstance(result, dict) and "text" in result:
225
+ text = result["text"].lower()
226
+ term_freq = sum(1 for term in query_terms if term in text)
227
+ normalized_score = term_freq / (len(text.split()) + 1) * 10
228
+ result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
229
+ elif isinstance(result, dict):
230
+ result["rerank_score"] = result.get("score", 0)
231
+ results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
232
+ return results
233
+ # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 2 ๋ ====================
234
+
235
+ # ReRanker ํด๋ž˜์Šค ์‚ฌ์šฉ
236
+ retriever = ReRanker(
237
+ base_retriever=base_retriever,
238
+ rerank_fn=custom_rerank_fn, # ์ด์ œ ํ•จ์ˆ˜๊ฐ€ ์ •์˜๋˜์—ˆ์œผ๋ฏ€๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
239
+ rerank_field="text"
240
+ )
241
+ logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
242
+ except Exception as e_rerank:
243
+ logger.error(f"์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_rerank}", exc_info=True)
244
+ logger.warning("์žฌ์ˆœ์œ„ํ™” ์‹คํŒจ, ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ retriever๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
245
+ retriever = base_retriever # fallback
246
+
247
+ logger.info("--- init_retriever ์ข…๋ฃŒ ---")
248
+ return retriever
249
+
250
+ def background_init():
251
+ """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์ˆ˜ํ–‰"""
252
+ global app_ready, retriever, base_retriever, llm_interface, stt_client
253
+
254
+ temp_app_ready = False # ์ž„์‹œ ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
255
+ try:
256
+ logger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘...")
257
+
258
+ # 1. LLM, STT ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” (ํ•„์š” ์‹œ)
259
+ if llm_interface is None or isinstance(llm_interface, MockComponent):
260
+ if 'LLMInterface' in globals() and LLMInterface != MockComponent:
261
+ llm_interface = LLMInterface(default_llm="openai")
262
+ logger.info("LLM ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
263
+ else:
264
+ logger.warning("LLMInterface ํด๋ž˜์Šค ์—†์Œ. Mock ์‚ฌ์šฉ.")
265
+ llm_interface = MockComponent() # Mock ๊ฐ์ฒด ๋ณด์žฅ
266
+ if stt_client is None or isinstance(stt_client, MockComponent):
267
+ if 'VitoSTT' in globals() and VitoSTT != MockComponent:
268
+ stt_client = VitoSTT()
269
+ logger.info("STT ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
270
+ else:
271
+ logger.warning("VitoSTT ํด๋ž˜์Šค ์—†์Œ. Mock ์‚ฌ์šฉ.")
272
+ stt_client = MockComponent() # Mock ๊ฐ์ฒด ๋ณด์žฅ
273
+
274
+
275
+ # 2. ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
276
+ if 'VectorRetriever' in globals() and VectorRetriever != MockComponent:
277
+ logger.info("์‹ค์ œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
278
+ # init_retriever๊ฐ€ base_retriever์™€ retriever๋ฅผ ๋ชจ๋‘ ์„ค์ •ํ•œ๋‹ค๊ณ  ๊ฐ€์ •
279
+ retriever = init_retriever()
280
+ # init_retriever ๋‚ด๋ถ€์—์„œ base_retriever๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์—ฌ๊ธฐ์„œ ์„ค์ •
281
+ if hasattr(retriever, 'base_retriever') and base_retriever is None:
282
+ base_retriever = retriever.base_retriever
283
+ elif base_retriever is None:
284
+ # retriever๊ฐ€ base_retriever๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ๋˜๋Š” ReRanker๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ
285
+ # init_retriever์—์„œ base_retriever๋ฅผ ์ง์ ‘ ์„ค์ •ํ•˜๋„๋ก ํ•˜๊ฑฐ๋‚˜, ์—ฌ๊ธฐ์„œ ๋ณ„๋„ ๋กœ์ง ํ•„์š”
286
+ # ์˜ˆ์‹œ: base_retriever = VectorRetriever.load(...) ๋˜๋Š” VectorRetriever()
287
+ logger.warning("init_retriever ํ›„ base_retriever๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ. ํ™•์ธ ํ•„์š”.")
288
+ # ์ž„์‹œ๋กœ retriever ์ž์ฒด๋ฅผ base_retriever๋กœ ์„ค์ • (๋™์ผ ๊ฐ์ฒด์ผ ๊ฒฝ์šฐ)
289
+ if isinstance(retriever, VectorRetriever):
290
+ base_retriever = retriever
291
+
292
+ # ์„ฑ๊ณต์ ์œผ๋กœ ์ดˆ๊ธฐํ™” ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ (None์ด ์•„๋‹Œ์ง€)
293
+ if retriever is not None and base_retriever is not None:
294
+ logger.info("๊ฒ€์ƒ‰๊ธฐ (Retriever, Base Retriever) ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
295
+ temp_app_ready = True # ์ดˆ๊ธฐํ™” ์„ฑ๊ณต ์‹œ์—๋งŒ True ์„ค์ •
296
+ else:
297
+ logger.error("๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ํ›„์—๋„ retriever ๋˜๋Š” base_retriever๊ฐ€ None์ž…๋‹ˆ๋‹ค.")
298
+ # ์‹คํŒจ ์‹œ Mock ๊ฐ์ฒด ํ• ๋‹น (์ตœ์†Œํ•œ์˜ ๋™์ž‘ ๋ณด์žฅ)
299
+ if base_retriever is None: base_retriever = MockComponent()
300
+ if retriever is None: retriever = MockComponent()
301
+ if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
302
+ if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
303
+ # temp_app_ready = False ๋˜๋Š” True (์ •์ฑ…์— ๋”ฐ๋ผ ๊ฒฐ์ •)
304
+ temp_app_ready = True # ์ผ๋‹จ ์•ฑ์€ ์‹คํ–‰๋˜๋„๋ก ์„ค์ •
305
+
306
+ else:
307
+ logger.warning("VectorRetriever ํด๏ฟฝ๏ฟฝ๏ฟฝ์Šค ์—†์Œ. Mock ๊ฒ€์ƒ‰๊ธฐ ์‚ฌ์šฉ.")
308
+ base_retriever = MockComponent()
309
+ retriever = MockComponent()
310
+ if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
311
+ if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
312
+ temp_app_ready = True # Mock์ด๋ผ๋„ ์ค€๋น„๋Š” ๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ
313
+
314
+ logger.info(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ. ์ตœ์ข… ์ƒํƒœ: {'Ready' if temp_app_ready else 'Not Ready (Error during init)'}")
315
+
316
+ except Exception as e:
317
+ logger.error(f"์•ฑ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
318
+ # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ์—๋„ Mock ๊ฐ์ฒด ํ• ๋‹น ์‹œ๋„
319
+ if base_retriever is None: base_retriever = MockComponent()
320
+ if retriever is None: retriever = MockComponent()
321
+ if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
322
+ if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
323
+ temp_app_ready = True # ์˜ค๋ฅ˜ ๋ฐœ์ƒํ•ด๋„ ์•ฑ์€ ์‘๋‹ตํ•˜๋„๋ก ์„ค์ • (์ •์ฑ…์— ๋”ฐ๋ผ False ๊ฐ€๋Šฅ)
324
+ logger.warning("์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์ง€๋งŒ Mock ๊ฐ์ฒด๋กœ ๋Œ€์ฒด ํ›„ ์•ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ •.")
325
+
326
+ finally:
327
+ # ์ตœ์ข…์ ์œผ๋กœ app_ready ์ƒํƒœ ์—…๋ฐ์ดํŠธ
328
+ app_ready = temp_app_ready
329
+
330
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ์‹œ์ž‘ ๋ถ€๋ถ„์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€
331
+ init_thread = threading.Thread(target=background_init)
332
+ init_thread.daemon = True
333
+ init_thread.start()
334
+
335
+
336
+ # --- Flask ๋ผ์šฐํŠธ ์ •์˜ ---
337
+
338
+ @app.route('/login', methods=['GET', 'POST'])
339
+ def login():
340
+ error = None
341
+ next_url = request.args.get('next') # ๋ฆฌ๋””๋ ‰์…˜ํ•  URL ๊ฐ€์ ธ์˜ค๊ธฐ
342
+ logger.info(f"-------------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ ‘์† (Next: {next_url}) --------------")
343
+ logger.info(f"Method: {request.method}")
344
+
345
+ # ํ—ค๋” ๋กœ๊น… (๋””๋ฒ„๊น…์šฉ)
346
+ # logger.debug("Request Headers:")
347
+ # for header, value in request.headers.items():
348
+ # logger.debug(f" {header}: {value}")
349
+
350
+ if request.method == 'POST':
351
+ logger.info("๋กœ๊ทธ์ธ ์‹œ๋„ ๋ฐ›์Œ")
352
+ username = request.form.get('username', '')
353
+ password = request.form.get('password', '')
354
+ logger.info(f"์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž๋ช…: {username}")
355
+ logger.info(f"๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์—ฌ๋ถ€: {len(password) > 0}")
356
+
357
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋น„๊ต
358
+ valid_username = ADMIN_USERNAME
359
+ valid_password = ADMIN_PASSWORD
360
+ logger.info(f"๊ฒ€์ฆ์šฉ ์‚ฌ์šฉ์ž๋ช…: {valid_username}")
361
+ logger.info(f"๊ฒ€์ฆ์šฉ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กด์žฌ ์—ฌ๋ถ€: {valid_password is not None and len(valid_password) > 0}")
362
+
363
+
364
+ if username == valid_username and password == valid_password:
365
+ logger.info(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต: {username}")
366
+ # ์„ธ์…˜ ์„ค์ • ์ „ ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ ๋กœ๊น…
367
+ logger.debug(f"์„ธ์…˜ ์„ค์ • ์ „: {session}")
368
+
369
+ # ์„ธ์…˜์— ๋กœ๊ทธ์ธ ์ •๋ณด ์ €์žฅ
370
+ session.permanent = True # PERMANENT_SESSION_LIFETIME ์„ค์ • ์‚ฌ์šฉ
371
+ session['logged_in'] = True
372
+ session['username'] = username
373
+ session.modified = True # ์„ธ์…˜์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Œ์„ ๋ช…์‹œ (ํ•„์ˆ˜๋Š” ์•„๋‹ ์ˆ˜ ์žˆ์Œ)
374
+
375
+ logger.info(f"์„ธ์…˜ ์„ค์ • ํ›„: {session}")
376
+ logger.info("์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ, ๋ฆฌ๋””๋ ‰์…˜ ์‹œ๋„")
377
+
378
+ # ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ๋ฆฌ๋””๋ ‰์…˜
379
+ # 'next' ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น URL๋กœ, ์—†์œผ๋ฉด ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ
380
+ redirect_to = next_url or url_for('index')
381
+ logger.info(f"๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์ƒ: {redirect_to}")
382
+ response = redirect(redirect_to)
383
+
384
+ # ์‘๋‹ต ํ—ค๋” ๋กœ๊น… (Set-Cookie ํ™•์ธ์šฉ)
385
+ # logger.debug(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‘๋‹ต ํ—ค๋”: {response.headers}")
386
+ return response
387
+ else:
388
+ logger.warning("๋กœ๊ทธ์ธ ์‹คํŒจ: ์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
389
+ if username != valid_username: logger.warning("์‚ฌ์šฉ์ž๋ช… ๋ถˆ์ผ์น˜")
390
+ if password != valid_password: logger.warning("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
391
+ error = '์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
392
+ else: # GET ์š”์ฒญ
393
+ logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET ์š”์ฒญ")
394
+ if 'logged_in' in session:
395
+ logger.info("์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž, ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
396
+ return redirect(url_for('index'))
397
+
398
+ logger.info("---------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง ----------")
399
+ return render_template('login.html', error=error, next=next_url)
400
+
401
+
402
+ @app.route('/logout')
403
+ def logout():
404
+ logger.info("-------------- ๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ --------------")
405
+ logger.info(f"๋กœ๊ทธ์•„์›ƒ ์ „ ์„ธ์…˜ ์ƒํƒœ: {session}")
406
+
407
+ if 'logged_in' in session:
408
+ username = session.get('username', 'unknown')
409
+ logger.info(f"์‚ฌ์šฉ์ž {username} ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
410
+ session.pop('logged_in', None)
411
+ session.pop('username', None)
412
+ session.modified = True # ์„ธ์…˜ ๏ฟฝ๏ฟฝ๏ฟฝ๊ฒฝ ๋ช…์‹œ
413
+ logger.info(f"์„ธ์…˜ ์ •๋ณด ์‚ญ์ œ ์™„๋ฃŒ. ํ˜„์žฌ ์„ธ์…˜: {session}")
414
+ else:
415
+ logger.warning("๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์•„์›ƒ ์‹œ๋„")
416
+
417
+ logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
418
+ response = redirect(url_for('login'))
419
+ # logger.debug(f"๋กœ๊ทธ์•„์›ƒ ์‘๋‹ต ํ—ค๋”: {response.headers}") # ์ฟ ํ‚ค ์‚ญ์ œ ํ™•์ธ์šฉ
420
+ return response
421
+
422
+
423
+ @app.route('/')
424
+ @login_required
425
+ def index():
426
+ """๋ฉ”์ธ ํŽ˜์ด์ง€"""
427
+ global app_ready
428
+
429
+ # ์•ฑ ์ค€๋น„ ์ƒํƒœ ํ™•์ธ - 30์ดˆ ์ด์ƒ ์ง€๋‚ฌ์œผ๋ฉด ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ
430
+ current_time = datetime.datetime.now()
431
+ start_time = datetime.datetime.fromtimestamp(os.path.getmtime(__file__))
432
+ time_diff = (current_time - start_time).total_seconds()
433
+
434
+ if not app_ready and time_diff > 30:
435
+ logger.warning(f"์•ฑ์ด 30์ดˆ ์ด์ƒ ์ดˆ๊ธฐํ™” ์ค‘ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.")
436
+ app_ready = True
437
+
438
+ if not app_ready:
439
+ logger.info("์•ฑ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋กœ๋”ฉ ํŽ˜์ด์ง€ ํ‘œ์‹œ")
440
+ return render_template('loading.html'), 503 # ์„œ๋น„์Šค ์ค€๋น„ ์•ˆ๋จ ์ƒํƒœ ์ฝ”๋“œ
441
+
442
+ logger.info("๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ")
443
+ return render_template('index.html')
444
+
445
+
446
+ @app.route('/api/status')
447
+ @login_required
448
+ def app_status():
449
+ """์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ API"""
450
+ logger.info(f"์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {'Ready' if app_ready else 'Not Ready'}")
451
+ return jsonify({"ready": app_ready})
452
+
453
+
454
+ @app.route('/api/llm', methods=['GET', 'POST'])
455
+ @login_required
456
+ def llm_api():
457
+ """์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ LLM ๋ชฉ๋ก ๋ฐ ์„ ํƒ API"""
458
+ global llm_interface
459
+
460
+ if not app_ready:
461
+ return jsonify({"error": "์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
462
+
463
+ if request.method == 'GET':
464
+ logger.info("LLM ๋ชฉ๋ก ์š”์ฒญ")
465
+ try:
466
+ current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
467
+ supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
468
+ supported_list = [{
469
+ "name": name, "id": id, "current": id == current_details.get("id")
470
+ } for name, id in supported_llms_dict.items()]
471
+
472
+ return jsonify({
473
+ "supported_llms": supported_list,
474
+ "current_llm": current_details
475
+ })
476
+ except Exception as e:
477
+ logger.error(f"LLM ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜: {e}")
478
+ return jsonify({"error": "LLM ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
479
+
480
+ elif request.method == 'POST':
481
+ data = request.get_json()
482
+ if not data or 'llm_id' not in data:
483
+ return jsonify({"error": "LLM ID๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
484
+
485
+ llm_id = data['llm_id']
486
+ logger.info(f"LLM ๋ณ€๊ฒฝ ์š”์ฒญ: {llm_id}")
487
+
488
+ try:
489
+ if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
490
+ raise NotImplementedError("LLM ์ธํ„ฐํŽ˜์ด์Šค์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ/์†์„ฑ ์—†์Œ")
491
+
492
+ if llm_id not in llm_interface.llm_clients:
493
+ return jsonify({"error": f"์ง€์›๋˜์ง€ ์•Š๋Š” LLM ID: {llm_id}"}), 400
494
+
495
+ success = llm_interface.set_llm(llm_id)
496
+ if success:
497
+ new_details = llm_interface.get_current_llm_details()
498
+ logger.info(f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
499
+ return jsonify({
500
+ "success": True,
501
+ "message": f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
502
+ "current_llm": new_details
503
+ })
504
+ else:
505
+ # set_llm ์ด False๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ (๊ฐ€๋Šฅ์„ฑ์€ ๋‚ฎ์ง€๋งŒ)
506
+ logger.error(f"LLM ๋ณ€๊ฒฝ ์‹คํŒจ (ID: {llm_id})")
507
+ return jsonify({"error": "LLM ๋ณ€๊ฒฝ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
508
+ except Exception as e:
509
+ logger.error(f"LLM ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}", exc_info=True)
510
+ return jsonify({"error": f"LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"}), 500
511
+
512
+
513
+ @app.route('/api/chat', methods=['POST'])
514
+ @login_required
515
+ def chat():
516
+ """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡ API"""
517
+ global retriever
518
+
519
+ if not app_ready or retriever is None:
520
+ return jsonify({"error": "์•ฑ/๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
521
+
522
+ try:
523
+ data = request.get_json()
524
+ if not data or 'query' not in data:
525
+ return jsonify({"error": "์ฟผ๋ฆฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
526
+
527
+ query = data['query']
528
+ logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...") # ๋„ˆ๋ฌด ๊ธด ์ฟผ๋ฆฌ ๋กœ๊ทธ๋Š” ์ž˜๋ผ์„œ ํ‘œ์‹œ
529
+
530
+ # RAG ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
531
+ if not hasattr(retriever, 'search'):
532
+ raise NotImplementedError("Retriever์— search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
533
+ search_results = retriever.search(query, top_k=5, first_stage_k=6) # ์žฌ์ˆœ์œ„ํ™” ๊ณ ๋ ค
534
+
535
+ # ์ปจํ…์ŠคํŠธ ์ค€๋น„
536
+ if not hasattr(DocumentProcessor, 'prepare_rag_context'):
537
+ raise NotImplementedError("DocumentProcessor์— prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
538
+ context = DocumentProcessor.prepare_rag_context(search_results, field="text")
539
+
540
+ if not context:
541
+ logger.warning("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•จ.")
542
+ # LLM ํ˜ธ์ถœ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ๋ฐ˜ํ™˜ ๋˜๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ์„ ์•Œ๋ฆฌ๋Š” ์‘๋‹ต ์ƒ์„ฑ
543
+ # answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." (์•„๋ž˜ LLM ํ˜ธ์ถœ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ)
544
+ pass
545
+
546
+ # LLM์— ์งˆ์˜
547
+ llm_id = data.get('llm_id', None) # ํด๋ผ์ด์–ธํŠธ์—์„œ ํŠน์ • LLM ์ง€์ • ๊ฐ€๋Šฅ
548
+ if not hasattr(llm_interface, 'rag_generate'):
549
+ raise NotImplementedError("LLMInterface์— rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
550
+
551
+ if not context:
552
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
553
+ logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
554
+ else:
555
+ answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
556
+ logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
557
+
558
+
559
+ # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ (CSV ID ์ถ”์ถœ ๋กœ์ง ํฌํ•จ)
560
+ sources = []
561
+ if search_results: # ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์†Œ์Šค ์ฒ˜๋ฆฌ
562
+ for result in search_results:
563
+ # ๊ฒฐ๊ณผ๊ฐ€ ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ์ธ์ง€ ํ™•์ธ
564
+ if not isinstance(result, dict):
565
+ logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
566
+ continue
567
+
568
+ if "source" in result:
569
+ source_info = {
570
+ "source": result.get("source", "Unknown"),
571
+ # ์žฌ์ˆœ์œ„ํ™” ์ ์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์›๋ž˜ ์ ์ˆ˜ ์‚ฌ์šฉ
572
+ "score": result.get("rerank_score", result.get("score", 0))
573
+ }
574
+
575
+ # CSV ํŒŒ์ผ ํŠน์ • ์ฒ˜๋ฆฌ
576
+ if "text" in result and result.get("filetype") == "csv":
577
+ try:
578
+ text_lines = result["text"].strip().split('\n')
579
+ if text_lines:
580
+ first_line = text_lines[0].strip()
581
+ if ',' in first_line:
582
+ first_column = first_line.split(',')[0].strip()
583
+ source_info["id"] = first_column # ์˜ˆ: CSV์˜ ์ฒซ ์ปฌ๋Ÿผ ๊ฐ’์„ ID๋กœ ์ถ”๊ฐ€
584
+ logger.debug(f"CSV ์†Œ์Šค ID ์ถ”์ถœ: {first_column} from {source_info['source']}")
585
+ except Exception as e:
586
+ logger.warning(f"CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({result.get('source')}): {e}")
587
+
588
+ sources.append(source_info)
589
+
590
+ # ์ตœ์ข… ์‘๋‹ต
591
+ response_data = {
592
+ "answer": answer,
593
+ "sources": sources,
594
+ "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
595
+ }
596
+ # logger.debug(f"์ตœ์ข… API ์‘๋‹ต: {response_data}") # ๋„ˆ๋ฌด ๊ธธ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ํ™œ์„ฑํ™”
597
+ return jsonify(response_data)
598
+
599
+ except Exception as e:
600
+ logger.error(f"์ฑ„ํŒ… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
601
+ return jsonify({"error": f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"}), 500
602
+
603
+
604
+ @app.route('/api/voice', methods=['POST'])
605
+ @login_required
606
+ def voice_chat():
607
+ """์Œ์„ฑ ์ฑ— API ์—”๋“œํฌ์ธํŠธ"""
608
+ global retriever, stt_client
609
+
610
+
611
+ if not app_ready:
612
+ logger.warning("์•ฑ ์ดˆ๊ธฐํ™”๊ฐ€ ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์ง€๋งŒ ์Œ์„ฑ API ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹œ๋„")
613
+ # ์—ฌ๊ธฐ์„œ ๋ฐ”๋กœ ๋ฆฌํ„ดํ•˜์ง€ ์•Š๊ณ  ๊ณ„์† ์ง„ํ–‰
614
+ # ์‚ฌ์ „ ๊ฒ€์‚ฌ: retriever์™€ stt_client๊ฐ€ ์ œ๋Œ€๋กœ ์ดˆ๊ธฐํ™”๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
615
+
616
+ if retriever is None:
617
+ logger.error("retriever๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค")
618
+ return jsonify({
619
+ "transcription": "(์Œ์„ฑ์„ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ–ˆ์ง€๋งŒ ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค)",
620
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
621
+ "sources": []
622
+ })
623
+ # ๋˜๋Š” ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์„ ๋•Œ๋งŒ ํŠน๋ณ„ ์‘๋‹ต ๋ฐ˜ํ™˜
624
+ if stt_client is None:
625
+ return jsonify({
626
+ "transcription": "(์Œ์„ฑ ์ธ์‹ ๊ธฐ๋Šฅ์ด ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค)",
627
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์Œ์„ฑ ์ธ์‹ ์„œ๋น„์Šค๊ฐ€ ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
628
+ "sources": []
629
+ })
630
+
631
+ logger.info("์Œ์„ฑ ์ฑ— ์š”์ฒญ ์ˆ˜์‹ ")
632
+
633
+ if 'audio' not in request.files:
634
+ logger.error("์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์Œ")
635
+ return jsonify({"error": "์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
636
+
637
+ audio_file = request.files['audio']
638
+ logger.info(f"์ˆ˜์‹ ๋œ ์˜ค๋””์˜ค ํŒŒ์ผ: {audio_file.filename} ({audio_file.content_type})")
639
+
640
+ try:
641
+ # ์˜ค๋””์˜ค ํŒŒ์ผ ์ฒ˜๋ฆฌ
642
+ # ์ž„์‹œ ํŒŒ์ผ ์‚ฌ์šฉ ๊ณ ๋ ค (๋ฉ”๋ชจ๋ฆฌ ๋ถ€๋‹ด ์ค„์ด๊ธฐ ์œ„ํ•ด)
643
+ with tempfile.NamedTemporaryFile(delete=True) as temp_audio:
644
+ audio_file.save(temp_audio.name)
645
+ logger.info(f"์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ž„์‹œ ์ €์žฅ: {temp_audio.name}")
646
+ # VitoSTT.transcribe_audio ๊ฐ€ ํŒŒ์ผ ๊ฒฝ๋กœ ๋˜๋Š” ๋ฐ”์ดํŠธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„๋˜์–ด์•ผ ํ•จ
647
+ # ์—ฌ๊ธฐ์„œ๋Š” ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •
648
+ if not hasattr(stt_client, 'transcribe_audio'):
649
+ raise NotImplementedError("STT ํด๋ผ์ด์–ธํŠธ์— transcribe_audio ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
650
+
651
+ # ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ ์ „๋‹ฌ ์‹œ
652
+ # stt_result = stt_client.transcribe_audio(temp_audio.name, language="ko")
653
+ # ๋ฐ”์ดํŠธ๋กœ ์ „๋‹ฌ ์‹œ
654
+ with open(temp_audio.name, 'rb') as f_bytes:
655
+ audio_bytes = f_bytes.read()
656
+ stt_result = stt_client.transcribe_audio(audio_bytes, language="ko")
657
+
658
+
659
+ if not isinstance(stt_result, dict) or not stt_result.get("success"):
660
+ error_msg = stt_result.get("error", "์•Œ ์ˆ˜ ์—†๋Š” STT ์˜ค๋ฅ˜") if isinstance(stt_result, dict) else "STT ๊ฒฐ๊ณผ ํ˜•์‹ ์˜ค๋ฅ˜"
661
+ logger.error(f"์Œ์„ฑ์ธ์‹ ์‹คํŒจ: {error_msg}")
662
+ return jsonify({
663
+ "error": "์Œ์„ฑ์ธ์‹ ์‹คํŒจ",
664
+ "details": error_msg
665
+ }), 500
666
+
667
+ transcription = stt_result.get("text", "")
668
+ if not transcription:
669
+ logger.warning("์Œ์„ฑ์ธ์‹ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.")
670
+ return jsonify({"error": "์Œ์„ฑ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.", "transcription": ""}), 400
671
+
672
+ logger.info(f"์Œ์„ฑ์ธ์‹ ์„ฑ๊ณต: {transcription[:50]}...")
673
+ if retriever is None:
674
+ logger.error("STT ์„ฑ๊ณต ํ›„ ๊ฒ€์ƒ‰ ์‹œ๋„ ์ค‘ retriever๊ฐ€ None์ž„")
675
+ return jsonify({
676
+ "transcription": transcription,
677
+ "answer": "์Œ์„ฑ์„ ์ธ์‹ํ–ˆ์ง€๋งŒ, ํ˜„์žฌ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์ด ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
678
+ "sources": []
679
+ })
680
+ # --- ์ดํ›„ ๋กœ์ง์€ /api/chat๊ณผ ๊ฑฐ์˜ ๋™์ผ ---
681
+ # RAG ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
682
+ search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
683
+ context = DocumentProcessor.prepare_rag_context(search_results, field="text")
684
+
685
+ if not context:
686
+ logger.warning("์Œ์„ฑ ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ.")
687
+ # answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." (์•„๋ž˜ LLM ํ˜ธ์ถœ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ)
688
+ pass
689
+
690
+ # LLM ํ˜ธ์ถœ
691
+ llm_id = request.form.get('llm_id', None) # ์Œ์„ฑ ์š”์ฒญ์€ form ๋ฐ์ดํ„ฐ๋กœ LLM ID ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ
692
+ if not context:
693
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
694
+ logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
695
+ else:
696
+ answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
697
+ logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
698
+
699
+
700
+ # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ
701
+ enhanced_sources = []
702
+ if search_results:
703
+ for doc in search_results:
704
+ if not isinstance(doc, dict): continue # ํ˜•์‹ ์ฒดํฌ
705
+ if "source" in doc:
706
+ source_info = {
707
+ "source": doc.get("source", "Unknown"),
708
+ "score": doc.get("rerank_score", doc.get("score", 0))
709
+ }
710
+ if "text" in doc and doc.get("filetype") == "csv":
711
+ try:
712
+ text_lines = doc["text"].strip().split('\n')
713
+ if text_lines:
714
+ first_line = text_lines[0].strip()
715
+ if ',' in first_line:
716
+ first_column = first_line.split(',')[0].strip()
717
+ source_info["id"] = first_column
718
+ except Exception as e:
719
+ logger.warning(f"[์Œ์„ฑ์ฑ—] CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({doc.get('source')}): {e}")
720
+ enhanced_sources.append(source_info)
721
+
722
+ # ์ตœ์ข… ์‘๋‹ต
723
+ response_data = {
724
+ "transcription": transcription,
725
+ "answer": answer,
726
+ "sources": enhanced_sources,
727
+ "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
728
+ }
729
+ return jsonify(response_data)
730
+
731
+ except Exception as e:
732
+ logger.error(f"์Œ์„ฑ ์ฑ— ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
733
+ return jsonify({
734
+ "error": "์Œ์„ฑ ์ฒ˜๋ฆฌ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ",
735
+ "details": str(e)
736
+ }), 500
737
+
738
+
739
+ @app.route('/api/upload', methods=['POST'])
740
+ @login_required
741
+ def upload_document():
742
+ """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ์—…๋กœ๋“œ API"""
743
+ global base_retriever, retriever
744
+
745
+ if not app_ready or base_retriever is None:
746
+ return jsonify({"error": "์•ฑ/๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค."}), 503
747
+
748
+ if 'document' not in request.files:
749
+ return jsonify({"error": "๋ฌธ์„œ ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
750
+
751
+ doc_file = request.files['document']
752
+ if doc_file.filename == '':
753
+ return jsonify({"error": "์„ ํƒ๋œ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค."}), 400
754
+
755
+ if not allowed_doc_file(doc_file.filename):
756
+ logger.error(f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹: {doc_file.filename}")
757
+ return jsonify({"error": f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ํ—ˆ์šฉ: {', '.join(ALLOWED_DOC_EXTENSIONS)}"}), 400
758
+
759
+ try:
760
+ filename = secure_filename(doc_file.filename)
761
+ filepath = os.path.join(app.config['DATA_FOLDER'], filename)
762
+ doc_file.save(filepath)
763
+ logger.info(f"๋ฌธ์„œ ์ €์žฅ ์™„๋ฃŒ: {filepath}")
764
+
765
+ # ๋ฌธ์„œ ์ฒ˜๋ฆฌ (์ธ์ฝ”๋”ฉ ์ฒ˜๋ฆฌ ํฌํ•จ)
766
+ try:
767
+ with open(filepath, 'r', encoding='utf-8') as f:
768
+ content = f.read()
769
+ except UnicodeDecodeError:
770
+ logger.info(f"UTF-8 ๋””์ฝ”๋”ฉ ์‹คํŒจ, CP949๋กœ ์‹œ๋„: {filename}")
771
+ try:
772
+ with open(filepath, 'r', encoding='cp949') as f:
773
+ content = f.read()
774
+ except Exception as e_cp949:
775
+ logger.error(f"CP949 ๋””์ฝ”๋”ฉ ์‹คํŒจ ({filename}): {e_cp949}")
776
+ return jsonify({"error": "ํŒŒ์ผ ์ธ์ฝ”๋”ฉ์„ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (UTF-8, CP949 ์‹œ๋„ ์‹คํŒจ)."}), 400
777
+ except Exception as e_read:
778
+ logger.error(f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜ ({filename}): {e_read}")
779
+ return jsonify({"error": f"ํŒŒ์ผ ์ฝ๊ธฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e_read)}"}), 500
780
+
781
+
782
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ ๋ฌธ์„œ ๋ถ„ํ• /์ฒ˜๋ฆฌ
783
+ metadata = {
784
+ "source": filename, "filename": filename,
785
+ "filetype": filename.rsplit('.', 1)[1].lower(),
786
+ "filepath": filepath
787
+ }
788
+ file_ext = metadata["filetype"]
789
+ docs = []
790
+
791
+ if not hasattr(DocumentProcessor, 'csv_to_documents') or not hasattr(DocumentProcessor, 'text_to_documents'):
792
+ raise NotImplementedError("DocumentProcessor์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ ์—†์Œ")
793
+
794
+ if file_ext == 'csv':
795
+ logger.info(f"CSV ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
796
+ docs = DocumentProcessor.csv_to_documents(content, metadata) # ํ–‰ ๋‹จ์œ„ ์ฒ˜๋ฆฌ ๊ฐ€์ •
797
+ else: # ๊ธฐํƒ€ ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๋ฌธ์„œ
798
+ logger.info(f"์ผ๋ฐ˜ ํ…์ŠคํŠธ ๋ฌธ์„œ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
799
+ # PDF, DOCX ๋“ฑ์€ ๋ณ„๋„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(pypdf, python-docx) ํ•„์š”
800
+ if file_ext in ['pdf', 'docx']:
801
+ logger.warning(f".{file_ext} ํŒŒ์ผ ์ฒ˜๋ฆฌ๋Š” ํ˜„์žฌ ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ์ถ”์ถœ ๋กœ์ง ์ถ”๊ฐ€ ํ•„์š”.")
802
+ # ์—ฌ๊ธฐ์— pdf/docx ํ…์ŠคํŠธ ์ถ”์ถœ ๋กœ์ง ์ถ”๊ฐ€
803
+ # ์˜ˆ: content = extract_text_from_pdf(filepath)
804
+ # content = extract_text_from_docx(filepath)
805
+ # ์ž„์‹œ๋กœ ๋น„์›Œ๋‘ 
806
+ content = ""
807
+
808
+ if content: # ํ…์ŠคํŠธ ๋‚ด์šฉ์ด ์žˆ์„ ๋•Œ๋งŒ ์ฒ˜๋ฆฌ
809
+ docs = DocumentProcessor.text_to_documents(
810
+ content, metadata=metadata,
811
+ chunk_size=512, chunk_overlap=50
812
+ )
813
+
814
+ # ๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ ์ถ”๊ฐ€ ๋ฐ ์ธ๋ฑ์Šค ์ €์žฅ
815
+ if docs:
816
+ if not hasattr(base_retriever, 'add_documents') or not hasattr(base_retriever, 'save'):
817
+ raise NotImplementedError("๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ์— add_documents ๋˜๋Š” save ๋ฉ”์†Œ๋“œ ์—†์Œ")
818
+
819
+ logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
820
+ base_retriever.add_documents(docs)
821
+
822
+ # ์ธ๋ฑ์Šค ์ €์žฅ (์—…๋กœ๋“œ๋งˆ๋‹ค ์ €์žฅ - ๋น„ํšจ์œจ์ ์ผ ์ˆ˜ ์žˆ์Œ)
823
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
824
+ index_path = app.config['INDEX_PATH']
825
+ try:
826
+ base_retriever.save(index_path)
827
+ logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
828
+ # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ๋„ ์—…๋ฐ์ดํŠธ ํ•„์š” ์‹œ ๋กœ์ง ์ถ”๊ฐ€
829
+ # ์˜ˆ: retriever.update_base_retriever(base_retriever)
830
+ return jsonify({
831
+ "success": True,
832
+ "message": f"ํŒŒ์ผ '{filename}' ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ({len(docs)}๊ฐœ ์ฒญํฌ ์ถ”๊ฐ€)."
833
+ })
834
+ except Exception as e_save:
835
+ logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e_save}")
836
+ return jsonify({"error": f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {str(e_save)}"}), 500
837
+ else:
838
+ logger.warning(f"ํŒŒ์ผ '{filename}'์—์„œ ์ฒ˜๋ฆฌ๏ฟฝ๏ฟฝ๏ฟฝ ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค.")
839
+ # ํŒŒ์ผ์€ ์ €์žฅ๋˜์—ˆ์œผ๋ฏ€๋กœ ์„ฑ๊ณต์œผ๋กœ ๊ฐ„์ฃผํ• ์ง€ ๊ฒฐ์ • ํ•„์š”
840
+ return jsonify({
841
+ "warning": True,
842
+ "message": f"ํŒŒ์ผ '{filename}'์ด ์ €์žฅ๋˜์—ˆ์ง€๋งŒ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."
843
+ })
844
+
845
+ except Exception as e:
846
+ logger.error(f"ํŒŒ์ผ ์—…๋กœ๋“œ ๋˜๋Š” ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
847
+ return jsonify({"error": f"ํŒŒ์ผ ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
848
+
849
+
850
+ @app.route('/api/documents', methods=['GET'])
851
+ @login_required
852
+ def list_documents():
853
+ """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ๋ชฉ๋ก API"""
854
+ global base_retriever
855
+
856
+ if not app_ready or base_retriever is None:
857
+ return jsonify({"error": "์•ฑ/๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค."}), 503
858
+
859
+ try:
860
+ sources = {}
861
+ total_chunks = 0
862
+ # base_retriever.documents ์™€ ๊ฐ™์€ ์†์„ฑ์ด ์‹ค์ œ ํด๋ž˜์Šค์— ์žˆ๋‹ค๊ณ  ๊ฐ€์ •
863
+ if hasattr(base_retriever, 'documents') and base_retriever.documents:
864
+ logger.info(f"์ด {len(base_retriever.documents)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ์—์„œ ์†Œ์Šค ๋ชฉ๋ก ์ƒ์„ฑ ์ค‘...")
865
+ for doc in base_retriever.documents:
866
+ # ๋ฌธ์„œ ์ฒญํฌ๊ฐ€ ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ๋ผ๊ณ  ๊ฐ€์ •
867
+ if not isinstance(doc, dict): continue
868
+
869
+ source = doc.get("source", "unknown") # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ source ๊ฐ€์ ธ์˜ค๊ธฐ
870
+ if source == "unknown" and "metadata" in doc and isinstance(doc["metadata"], dict):
871
+ source = doc["metadata"].get("source", "unknown") # Langchain Document ๊ตฌ์กฐ ๊ณ ๋ ค
872
+
873
+ if source != "unknown":
874
+ if source in sources:
875
+ sources[source]["chunks"] += 1
876
+ else:
877
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ์ถ”๊ฐ€ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
878
+ filename = doc.get("filename", source)
879
+ filetype = doc.get("filetype", "unknown")
880
+ if "metadata" in doc and isinstance(doc["metadata"], dict):
881
+ filename = doc["metadata"].get("filename", filename)
882
+ filetype = doc["metadata"].get("filetype", filetype)
883
+
884
+ sources[source] = {
885
+ "filename": filename,
886
+ "chunks": 1,
887
+ "filetype": filetype
888
+ }
889
+ total_chunks += 1
890
+ else:
891
+ logger.info("๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ๊ฐ€ ์—†๊ฑฐ๋‚˜ documents ์†์„ฑ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
892
+
893
+ # ๋ชฉ๋ก ํ˜•์‹ ๋ณ€ํ™˜ ๋ฐ ์ •๋ ฌ
894
+ documents = [{"source": src, **info} for src, info in sources.items()]
895
+ documents.sort(key=lambda x: x["chunks"], reverse=True)
896
+
897
+ logger.info(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์™„๋ฃŒ: {len(documents)}๊ฐœ ์†Œ์Šค ํŒŒ์ผ, {total_chunks}๊ฐœ ์ฒญํฌ")
898
+ return jsonify({
899
+ "documents": documents,
900
+ "total_documents": len(documents),
901
+ "total_chunks": total_chunks # sum(doc["chunks"] for doc in documents) ์™€ ๋™์ผ
902
+ })
903
+
904
+ except Exception as e:
905
+ logger.error(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
906
+ return jsonify({"error": f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
907
+
908
+
909
+ # ์ •์  ํŒŒ์ผ ์„œ๋น™
910
+ @app.route('/static/<path:path>')
911
+ def send_static(path):
912
+ return send_from_directory('static', path)
913
+
914
+
915
+ # --- ์š”์ฒญ ์ฒ˜๋ฆฌ ํ›… ---
916
+
917
+ # @app.before_request - ์ œ๊ฑฐ๋จ (์ˆ˜๋™ ์ฟ ํ‚ค ์ฒ˜๋ฆฌ ๋กœ์ง ์‚ญ์ œ)
918
+ # def process_cookies(): ...
919
+
920
+ @app.after_request
921
+ def after_request_func(response):
922
+ """๋ชจ๋“  ์‘๋‹ต์— ๋Œ€ํ•ด ํ›„์ฒ˜๋ฆฌ ์ˆ˜ํ–‰"""
923
+ # ์„ธ์…˜์ด ์ˆ˜์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ ํ›„ ๋กœ๊น… (๋””๋ฒ„๊น…์šฉ)
924
+ # if session.modified: # session.modified ๋Š” ํ•ญ์ƒ ์ •ํ™•ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ
925
+ # logger.debug(f"[After Request] ์„ธ์…˜ ์ˆ˜์ • ๊ฐ์ง€๋จ. ์‘๋‹ต์— Set-Cookie ํฌํ•จ ์—ฌ๋ถ€ ํ™•์ธ ํ•„์š”.")
926
+ # logger.debug(f"[After Request] ์‘๋‹ต ํ—ค๋”: {response.headers}") # ๋””๋ฒ„๊น… ์‹œ Set-Cookie ํ™•์ธ
927
+ return response # ์‘๋‹ต ๊ฐ์ฒด๋ฅผ ๋ฐ˜๋“œ์‹œ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
928
+
929
+ # ์•ฑ ์‹คํ–‰ (๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ)
930
+ if __name__ == '__main__':
931
+ logger.info("Flask ์•ฑ์„ ์ง์ ‘ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„).")
932
+ # ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๋Š” ์‹ค์ œ ๋ฐฐํฌ ์‹œ False๋กœ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
933
+ # port ๋ฒˆํ˜ธ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
934
+ port = int(os.environ.get("PORT", 7860))
935
+ logger.info(f"์„œ๋ฒ„๋ฅผ http://0.0.0.0:{port} ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
936
+ app.run(debug=True, host='0.0.0.0', port=port)
app/static/js/app.js CHANGED
@@ -1,58 +1,636 @@
1
  /**
2
- * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI JavaScript ๋ฉ”์ธ ํŒŒ์ผ
3
- * ์ด ํŒŒ์ผ์€ ๋ชจ๋“ˆํ™”๋œ JS ํŒŒ์ผ๋“ค์„ ๋กœ๋“œํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
4
  */
5
 
6
- // ์Šคํฌ๋ฆฝํŠธ ๋™์  ๋กœ๋“œ ํ•จ์ˆ˜
7
- function loadScript(url, callback = null) {
8
- console.log(`์Šคํฌ๋ฆฝํŠธ ๋กœ๋“œ ์ค‘: ${url}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- const script = document.createElement('script');
11
- script.type = 'text/javascript';
12
- script.src = url;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- // ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ์„ค์ • (์žˆ๋Š” ๊ฒฝ์šฐ)
15
- if (callback) {
16
- script.onload = callback;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
 
 
 
 
 
 
 
 
 
 
18
 
19
- // ์—๋Ÿฌ ํ•ธ๋“ค๋ง
20
- script.onerror = function() {
21
- console.error(`์Šคํฌ๋ฆฝํŠธ ๋กœ๋“œ ์‹คํŒจ: ${url}`);
22
- };
23
 
24
- // ํŽ˜์ด์ง€์— ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€
25
- document.head.appendChild(script);
26
  }
27
 
28
- // ๋ชจ๋“ˆ ๋กœ๋“œ ์ˆœ์„œ ๊ด€๋ฆฌ
29
- document.addEventListener('DOMContentLoaded', function() {
30
- console.log('์•ฑ ์ดˆ๊ธฐํ™” ์‹œ์ž‘: ๋ชจ๋“ˆ ๋กœ๋“œ');
 
 
 
 
 
31
 
32
- // ๋ชจ๋“ˆ ํŒŒ์ผ ๊ฒฝ๋กœ
33
- const coreScriptPath = '/static/js/app-core.js';
34
- const llmScriptPath = '/static/js/app-llm.js';
35
- const docsScriptPath = '/static/js/app-docs.js';
36
- const deviceScriptPath = '/static/js/app-device.js';
37
 
38
- // ๋ชจ๋“ˆ ์ˆœ์ฐจ์  ๋กœ๋“œ
39
- loadScript(coreScriptPath, function() {
40
- console.log('์ฝ”์–ด ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- // LLM ๋ชจ๋“ˆ ๋กœ๋“œ
43
- loadScript(llmScriptPath, function() {
44
- console.log('LLM ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
 
 
 
 
 
45
 
46
- // ๋ฌธ์„œ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๋กœ๋“œ
47
- loadScript(docsScriptPath, function() {
48
- console.log('๋ฌธ์„œ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
49
-
50
- // ์žฅ์น˜ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๋กœ๋“œ
51
- loadScript(deviceScriptPath, function() {
52
- console.log('์žฅ์น˜ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
53
- console.log('๋ชจ๋“  ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
54
- });
55
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  });
57
- });
58
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI JavaScript
 
3
  */
4
 
5
+ // DOM ์š”์†Œ
6
+ const chatTab = document.getElementById('chatTab');
7
+ const docsTab = document.getElementById('docsTab');
8
+ const chatSection = document.getElementById('chatSection');
9
+ const docsSection = document.getElementById('docsSection');
10
+ const chatMessages = document.getElementById('chatMessages');
11
+ const userInput = document.getElementById('userInput');
12
+ const sendButton = document.getElementById('sendButton');
13
+ const micButton = document.getElementById('micButton');
14
+ const stopRecordingButton = document.getElementById('stopRecordingButton');
15
+ const recordingStatus = document.getElementById('recordingStatus');
16
+ const uploadForm = document.getElementById('uploadForm');
17
+ const documentFile = document.getElementById('documentFile');
18
+ const fileName = document.getElementById('fileName');
19
+ const uploadButton = document.getElementById('uploadButton');
20
+ const uploadStatus = document.getElementById('uploadStatus');
21
+ const refreshDocsButton = document.getElementById('refreshDocsButton');
22
+ const docsList = document.getElementById('docsList');
23
+ const docsLoading = document.getElementById('docsLoading');
24
+ const noDocsMessage = document.getElementById('noDocsMessage');
25
+ const llmSelect = document.getElementById('llmSelect');
26
+ const currentLLMInfo = document.getElementById('currentLLMInfo');
27
+
28
+ // LLM ๊ด€๋ จ ๋ณ€์ˆ˜
29
+ let currentLLM = 'openai';
30
+ let supportedLLMs = [];
31
+
32
+ // ๋…น์Œ ๊ด€๋ จ ๋ณ€์ˆ˜
33
+ let mediaRecorder = null;
34
+ let audioChunks = [];
35
+ let isRecording = false;
36
+
37
+ // ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ํ•จ์ˆ˜
38
+ async function checkAppStatus() {
39
+ try {
40
+ const response = await fetch('/api/status');
41
+ if (!response.ok) {
42
+ return false;
43
+ }
44
+ const data = await response.json();
45
+ return data.ready;
46
+ } catch (error) {
47
+ console.error('Status check failed:', error);
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * LLM ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
54
+ */
55
+ async function loadLLMs() {
56
+ try {
57
+ // API ์š”์ฒญ
58
+ const response = await fetch('/api/llm');
59
+
60
+ if (!response.ok) {
61
+ throw new Error(`HTTP error! status: ${response.status}`);
62
+ }
63
+
64
+ const data = await response.json();
65
+ supportedLLMs = data.supported_llms;
66
+ currentLLM = data.current_llm.id;
67
+
68
+ // LLM ์„ ํƒ ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
69
+ llmSelect.innerHTML = '';
70
+ supportedLLMs.forEach(llm => {
71
+ const option = document.createElement('option');
72
+ option.value = llm.id;
73
+ option.textContent = llm.name;
74
+ option.selected = llm.current;
75
+ llmSelect.appendChild(option);
76
+ });
77
+
78
+ // ํ˜„์žฌ LLM ํ‘œ์‹œ
79
+ updateCurrentLLMInfo(data.current_llm);
80
+ } catch (error) {
81
+ console.error('LLM ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:', error);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * LLM ๋ณ€๊ฒฝ ํ•จ์ˆ˜
87
+ * @param {string} llmId - ๋ณ€๊ฒฝํ•  LLM ID
88
+ */
89
+ async function changeLLM(llmId) {
90
+ try {
91
+ // API ์š”์ฒญ
92
+ const response = await fetch('/api/llm', {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json'
96
+ },
97
+ body: JSON.stringify({ llm_id: llmId })
98
+ });
99
+
100
+ if (!response.ok) {
101
+ throw new Error(`HTTP error! status: ${response.status}`);
102
+ }
103
+
104
+ const data = await response.json();
105
+
106
+ if (data.success) {
107
+ currentLLM = llmId;
108
+ updateCurrentLLMInfo(data.current_llm);
109
+ console.log(`LLM์ด ${data.current_llm.name}(์œผ)๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`);
110
+
111
+ // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
112
+ const systemMessage = `LLM์ด ${data.current_llm.name}(์œผ)๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋ธ: ${data.current_llm.model}`;
113
+ addSystemNotification(systemMessage);
114
+ } else if (data.error) {
115
+ console.error('LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜:', data.error);
116
+ alert(`LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜: ${data.error}`);
117
+ }
118
+ } catch (error) {
119
+ console.error('LLM ๋ณ€๊ฒฝ ์‹คํŒจ:', error);
120
+ alert('LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
121
+ }
122
+ }
123
+
124
+ /**
125
+ * ํ˜„์žฌ LLM ์ •๋ณด ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
126
+ * @param {Object} llmInfo - LLM ์ •๋ณด ๊ฐ์ฒด
127
+ */
128
+ function updateCurrentLLMInfo(llmInfo) {
129
+ if (currentLLMInfo) {
130
+ currentLLMInfo.textContent = `${llmInfo.name} (${llmInfo.model})`;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * ์‹œ์Šคํ…œ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
136
+ * @param {string} message - ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€
137
+ */
138
+ function addSystemNotification(message) {
139
+ const messageDiv = document.createElement('div');
140
+ messageDiv.classList.add('message', 'system');
141
+
142
+ const contentDiv = document.createElement('div');
143
+ contentDiv.classList.add('message-content');
144
+
145
+ const messageP = document.createElement('p');
146
+ messageP.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
147
+ contentDiv.appendChild(messageP);
148
+
149
+ messageDiv.appendChild(contentDiv);
150
+ chatMessages.appendChild(messageDiv);
151
+
152
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
153
+ chatMessages.scrollTop = chatMessages.scrollHeight;
154
+ }
155
+
156
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™”
157
+ document.addEventListener('DOMContentLoaded', () => {
158
+ // ์•ฑ ์ƒํƒœ ํ™•์ธ (๋กœ๋”ฉ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ)
159
+ if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) {
160
+ // ์•ฑ ์ƒํƒœ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
161
+ const statusInterval = setInterval(async () => {
162
+ const isReady = await checkAppStatus();
163
+ if (isReady) {
164
+ clearInterval(statusInterval);
165
+ console.log('์•ฑ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
166
+
167
+ // ์•ฑ์ด ์ค€๋น„๋˜๋ฉด LLM ๋ชฉ๋ก ๋กœ๋“œ
168
+ loadLLMs();
169
+ }
170
+ }, 5000);
171
+ }
172
+
173
+ // ํƒญ ์ „ํ™˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
174
+ chatTab.addEventListener('click', () => {
175
+ switchTab('chat');
176
+ });
177
+
178
+ docsTab.addEventListener('click', () => {
179
+ switchTab('docs');
180
+ loadDocuments();
181
+ });
182
+
183
+ // LLM ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
184
+ llmSelect.addEventListener('change', (event) => {
185
+ changeLLM(event.target.value);
186
+ });
187
+
188
+ // ๋ฉ”์‹œ์ง€ ์ „์†ก ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
189
+ sendButton.addEventListener('click', sendMessage);
190
+ userInput.addEventListener('keydown', (event) => {
191
+ if (event.key === 'Enter' && !event.shiftKey) {
192
+ event.preventDefault();
193
+ sendMessage();
194
+ }
195
+ });
196
+
197
+ // ์Œ์„ฑ ์ธ์‹ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
198
+ micButton.addEventListener('click', startRecording);
199
+ stopRecordingButton.addEventListener('click', stopRecording);
200
+
201
+ // ๋ฌธ์„œ ์—…๋กœ๋“œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
202
+ documentFile.addEventListener('change', (event) => {
203
+ if (event.target.files.length > 0) {
204
+ fileName.textContent = event.target.files[0].name;
205
+ } else {
206
+ fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
207
+ }
208
+ });
209
+
210
+ uploadForm.addEventListener('submit', (event) => {
211
+ event.preventDefault();
212
+ uploadDocument();
213
+ });
214
+
215
+ // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
216
+ refreshDocsButton.addEventListener('click', loadDocuments);
217
+
218
+ // ์ž๋™ ์ž…๋ ฅ ํ•„๋“œ ํฌ๊ธฐ ์กฐ์ •
219
+ userInput.addEventListener('input', adjustTextareaHeight);
220
+
221
+ // ์ดˆ๊ธฐ ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ
222
+ if (docsSection.classList.contains('active')) {
223
+ loadDocuments();
224
+ }
225
+ });
226
+
227
+ /**
228
+ * ํƒญ ์ „ํ™˜ ํ•จ์ˆ˜
229
+ * @param {string} tabName - ํ™œ์„ฑํ™”ํ•  ํƒญ ์ด๋ฆ„ ('chat' ๋˜๋Š” 'docs')
230
+ */
231
+ function switchTab(tabName) {
232
+ if (tabName === 'chat') {
233
+ chatTab.classList.add('active');
234
+ docsTab.classList.remove('active');
235
+ chatSection.classList.add('active');
236
+ docsSection.classList.remove('active');
237
+ } else if (tabName === 'docs') {
238
+ chatTab.classList.remove('active');
239
+ docsTab.classList.add('active');
240
+ chatSection.classList.remove('active');
241
+ docsSection.classList.add('active');
242
+ }
243
+ }
244
+
245
+ /**
246
+ * ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
247
+ */
248
+ async function sendMessage() {
249
+ const message = userInput.value.trim();
250
+ if (!message) return;
251
+
252
+ // UI ์—…๋ฐ์ดํŠธ
253
+ addMessage(message, 'user');
254
+ userInput.value = '';
255
+ adjustTextareaHeight();
256
+
257
+ // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
258
+ const loadingMessageId = addLoadingMessage();
259
 
260
+ try {
261
+ // API ์š”์ฒญ
262
+ const response = await fetch('/api/chat', {
263
+ method: 'POST',
264
+ headers: {
265
+ 'Content-Type': 'application/json'
266
+ },
267
+ body: JSON.stringify({
268
+ query: message,
269
+ llm_id: currentLLM // ํ˜„์žฌ ์„ ํƒ๋œ LLM ์ „์†ก
270
+ })
271
+ });
272
+
273
+ if (!response.ok) {
274
+ throw new Error(`HTTP error! status: ${response.status}`);
275
+ }
276
+
277
+ const data = await response.json();
278
+
279
+ // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
280
+ removeLoadingMessage(loadingMessageId);
281
+
282
+ // ์‘๋‹ต ํ‘œ์‹œ
283
+ if (data.error) {
284
+ addErrorMessage(data.error);
285
+ } else {
286
+ // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
287
+ if (data.llm) {
288
+ updateCurrentLLMInfo(data.llm);
289
+ }
290
+ addMessage(data.answer, 'bot', null, data.sources);
291
+ }
292
+ } catch (error) {
293
+ console.error('Error:', error);
294
+ removeLoadingMessage(loadingMessageId);
295
+ addErrorMessage('์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
296
+ }
297
+ }
298
+
299
+ /**
300
+ * ์Œ์„ฑ ๋…น์Œ ์‹œ์ž‘ ํ•จ์ˆ˜
301
+ */
302
+ async function startRecording() {
303
+ if (isRecording) return;
304
 
305
+ try {
306
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
307
+ isRecording = true;
308
+ audioChunks = [];
309
+
310
+ mediaRecorder = new MediaRecorder(stream);
311
+
312
+ mediaRecorder.addEventListener('dataavailable', (event) => {
313
+ if (event.data.size > 0) audioChunks.push(event.data);
314
+ });
315
+
316
+ mediaRecorder.addEventListener('stop', sendAudioMessage);
317
+
318
+ // ๋…น์Œ ์‹œ์ž‘
319
+ mediaRecorder.start();
320
+
321
+ // UI ์—…๋ฐ์ดํŠธ
322
+ micButton.style.display = 'none';
323
+ recordingStatus.classList.remove('hidden');
324
+
325
+ console.log('๋…น์Œ ์‹œ์ž‘๋จ');
326
+ } catch (error) {
327
+ console.error('์Œ์„ฑ ๋…น์Œ ๊ถŒํ•œ์„ ์–ป์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:', error);
328
+ alert('๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');
329
  }
330
+ }
331
+
332
+ /**
333
+ * ์Œ์„ฑ ๋…น์Œ ์ค‘์ง€ ํ•จ์ˆ˜
334
+ */
335
+ function stopRecording() {
336
+ if (!isRecording || !mediaRecorder) return;
337
+
338
+ mediaRecorder.stop();
339
+ isRecording = false;
340
 
341
+ // UI ์—…๋ฐ์ดํŠธ
342
+ micButton.style.display = 'flex';
343
+ recordingStatus.classList.add('hidden');
 
344
 
345
+ console.log('๋…น์Œ ์ค‘์ง€๋จ');
 
346
  }
347
 
348
+ /**
349
+ * ๋…น์Œ๋œ ์˜ค๋””์˜ค ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
350
+ */
351
+ async function sendAudioMessage() {
352
+ if (audioChunks.length === 0) return;
353
+
354
+ // ์˜ค๋””์˜ค Blob ์ƒ์„ฑ
355
+ const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
356
 
357
+ // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
358
+ const loadingMessageId = addLoadingMessage();
 
 
 
359
 
360
+ try {
361
+ // FormData์— ์˜ค๋””์˜ค ์ถ”๊ฐ€
362
+ const formData = new FormData();
363
+ formData.append('audio', audioBlob, 'recording.wav');
364
+ // ํ˜„์žฌ ์„ ํƒ๋œ LLM ์ถ”๊ฐ€
365
+ formData.append('llm_id', currentLLM);
366
+
367
+ // API ์š”์ฒญ
368
+ const response = await fetch('/api/voice', {
369
+ method: 'POST',
370
+ body: formData
371
+ });
372
+
373
+ if (!response.ok) {
374
+ throw new Error(`HTTP error! status: ${response.status}`);
375
+ }
376
+
377
+ const data = await response.json();
378
+
379
+ // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
380
+ removeLoadingMessage(loadingMessageId);
381
 
382
+ // ์‘๋‹ต ํ‘œ์‹œ
383
+ if (data.error) {
384
+ addErrorMessage(data.error);
385
+ } else {
386
+ // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
387
+ if (data.llm) {
388
+ updateCurrentLLMInfo(data.llm);
389
+ }
390
 
391
+ // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€(์Œ์„ฑ ํ…์ŠคํŠธ) ์ถ”๊ฐ€
392
+ if (data.transcription) {
393
+ addMessage(data.transcription, 'user');
394
+ }
395
+
396
+ // ๋ด‡ ์‘๋‹ต ์ถ”๊ฐ€
397
+ addMessage(data.answer, 'bot', data.transcription, data.sources);
398
+ }
399
+ } catch (error) {
400
+ console.error('Error:', error);
401
+ removeLoadingMessage(loadingMessageId);
402
+ addErrorMessage('์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
403
+ }
404
+ }
405
+
406
+ /**
407
+ * ๋ฌธ์„œ ์—…๋กœ๋“œ ํ•จ์ˆ˜
408
+ */
409
+ async function uploadDocument() {
410
+ if (documentFile.files.length === 0) {
411
+ alert('ํŒŒ์ผ์„ ์„ ํƒํ•ด ์ฃผ์„ธ์š”.');
412
+ return;
413
+ }
414
+
415
+ // UI ์—…๋ฐ์ดํŠธ
416
+ uploadStatus.classList.remove('hidden');
417
+ uploadStatus.className = 'upload-status';
418
+ uploadStatus.innerHTML = '<div class="spinner"></div><p>์—…๋กœ๋“œ ์ค‘...</p>';
419
+ uploadButton.disabled = true;
420
+
421
+ try {
422
+ const formData = new FormData();
423
+ formData.append('document', documentFile.files[0]);
424
+
425
+ // API ์š”์ฒญ
426
+ const response = await fetch('/api/upload', {
427
+ method: 'POST',
428
+ body: formData
429
  });
430
+
431
+ const data = await response.json();
432
+
433
+ // ์‘๋‹ต ์ฒ˜๋ฆฌ
434
+ if (data.error) {
435
+ uploadStatus.className = 'upload-status error';
436
+ uploadStatus.textContent = `์˜ค๋ฅ˜: ${data.error}`;
437
+ } else if (data.warning) {
438
+ uploadStatus.className = 'upload-status warning';
439
+ uploadStatus.textContent = data.message;
440
+ } else {
441
+ uploadStatus.className = 'upload-status success';
442
+ uploadStatus.textContent = data.message;
443
+
444
+ // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
445
+ loadDocuments();
446
+
447
+ // ์ž…๋ ฅ ํ•„๋“œ ์ดˆ๊ธฐํ™”
448
+ documentFile.value = '';
449
+ fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
450
+ }
451
+ } catch (error) {
452
+ console.error('Error:', error);
453
+ uploadStatus.className = 'upload-status error';
454
+ uploadStatus.textContent = '์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.';
455
+ } finally {
456
+ uploadButton.disabled = false;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
462
+ */
463
+ async function loadDocuments() {
464
+ // UI ์—…๋ฐ์ดํŠธ
465
+ docsList.querySelector('tbody').innerHTML = '';
466
+ docsLoading.classList.remove('hidden');
467
+ noDocsMessage.classList.add('hidden');
468
+
469
+ try {
470
+ // API ์š”์ฒญ
471
+ const response = await fetch('/api/documents');
472
+
473
+ if (!response.ok) {
474
+ throw new Error(`HTTP error! status: ${response.status}`);
475
+ }
476
+
477
+ const data = await response.json();
478
+
479
+ // ์‘๋‹ต ์ฒ˜๋ฆฌ
480
+ docsLoading.classList.add('hidden');
481
+
482
+ if (!data.documents || data.documents.length === 0) {
483
+ noDocsMessage.classList.remove('hidden');
484
+ return;
485
+ }
486
+
487
+ // ๋ฌธ์„œ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ
488
+ const tbody = docsList.querySelector('tbody');
489
+ data.documents.forEach(doc => {
490
+ const row = document.createElement('tr');
491
+
492
+ const fileNameCell = document.createElement('td');
493
+ fileNameCell.textContent = doc.filename || doc.source;
494
+ row.appendChild(fileNameCell);
495
+
496
+ const chunksCell = document.createElement('td');
497
+ chunksCell.textContent = doc.chunks;
498
+ row.appendChild(chunksCell);
499
+
500
+ const typeCell = document.createElement('td');
501
+ typeCell.textContent = doc.filetype || '-';
502
+ row.appendChild(typeCell);
503
+
504
+ tbody.appendChild(row);
505
+ });
506
+ } catch (error) {
507
+ console.error('Error:', error);
508
+ docsLoading.classList.add('hidden');
509
+ noDocsMessage.classList.remove('hidden');
510
+ noDocsMessage.querySelector('p').textContent = '๋ฌธ์„œ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.';
511
+ }
512
+ }
513
+
514
+ /**
515
+ * ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
516
+ * @param {string} text - ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
517
+ * @param {string} sender - ๋ฉ”์‹œ์ง€ ๋ฐœ์‹ ์ž ('user' ๋˜๋Š” 'bot' ๋˜๋Š” 'system')
518
+ * @param {string|null} transcription - ์Œ์„ฑ ์ธ์‹ ํ…์ŠคํŠธ (์„ ํƒ ์‚ฌํ•ญ)
519
+ * @param {Array|null} sources - ์†Œ์Šค ์ •๋ณด ๋ฐฐ์—ด (์„ ํƒ ์‚ฌํ•ญ)
520
+ */
521
+ function addMessage(text, sender, transcription = null, sources = null) {
522
+ const messageDiv = document.createElement('div');
523
+ messageDiv.classList.add('message', sender);
524
+
525
+ const contentDiv = document.createElement('div');
526
+ contentDiv.classList.add('message-content');
527
+
528
+ // ์Œ์„ฑ ์ธ์‹ ํ…์ŠคํŠธ ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
529
+ if (transcription && sender === 'bot') {
530
+ const transcriptionP = document.createElement('p');
531
+ transcriptionP.classList.add('transcription');
532
+ transcriptionP.textContent = `"${transcription}"`;
533
+ contentDiv.appendChild(transcriptionP);
534
+ }
535
+
536
+ // ๋ฉ”์‹œ์ง€ ํ…์ŠคํŠธ ์ถ”๊ฐ€
537
+ const textP = document.createElement('p');
538
+ textP.textContent = text;
539
+ contentDiv.appendChild(textP);
540
+
541
+ // ์†Œ์Šค ์ •๋ณด ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
542
+ if (sources && sources.length > 0 && sender === 'bot') {
543
+ const sourcesDiv = document.createElement('div');
544
+ sourcesDiv.classList.add('sources');
545
+
546
+ const sourcesTitle = document.createElement('strong');
547
+ sourcesTitle.textContent = '์ถœ์ฒ˜: ';
548
+ sourcesDiv.appendChild(sourcesTitle);
549
+
550
+ sources.forEach((source, index) => {
551
+ if (index < 3) { // ์ตœ๋Œ€ 3๊ฐœ๊นŒ์ง€๋งŒ ํ‘œ์‹œ
552
+ const sourceSpan = document.createElement('span');
553
+ sourceSpan.classList.add('source-item');
554
+ sourceSpan.textContent = source.source;
555
+ sourcesDiv.appendChild(sourceSpan);
556
+ }
557
+ });
558
+
559
+ contentDiv.appendChild(sourcesDiv);
560
+ }
561
+
562
+ messageDiv.appendChild(contentDiv);
563
+ chatMessages.appendChild(messageDiv);
564
+
565
+ // ์Šคํฌ๋กค์„ ๏ฟฝ๏ฟฝ๏ฟฝ์žฅ ์•„๋ž˜๋กœ ์ด๋™
566
+ chatMessages.scrollTop = chatMessages.scrollHeight;
567
+ }
568
+
569
+ /**
570
+ * ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
571
+ * @returns {string} ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
572
+ */
573
+ function addLoadingMessage() {
574
+ const id = 'loading-' + Date.now();
575
+ const messageDiv = document.createElement('div');
576
+ messageDiv.classList.add('message', 'bot');
577
+ messageDiv.id = id;
578
+
579
+ const contentDiv = document.createElement('div');
580
+ contentDiv.classList.add('message-content');
581
+
582
+ const loadingP = document.createElement('p');
583
+ loadingP.innerHTML = '<div class="spinner" style="width: 20px; height: 20px; display: inline-block; margin-right: 10px;"></div> ์ƒ๊ฐ ์ค‘...';
584
+ contentDiv.appendChild(loadingP);
585
+
586
+ messageDiv.appendChild(contentDiv);
587
+ chatMessages.appendChild(messageDiv);
588
+
589
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
590
+ chatMessages.scrollTop = chatMessages.scrollHeight;
591
+
592
+ return id;
593
+ }
594
+
595
+ /**
596
+ * ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ ํ•จ์ˆ˜
597
+ * @param {string} id - ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
598
+ */
599
+ function removeLoadingMessage(id) {
600
+ const loadingMessage = document.getElementById(id);
601
+ if (loadingMessage) {
602
+ loadingMessage.remove();
603
+ }
604
+ }
605
+
606
+ /**
607
+ * ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
608
+ * @param {string} errorText - ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
609
+ */
610
+ function addErrorMessage(errorText) {
611
+ const messageDiv = document.createElement('div');
612
+ messageDiv.classList.add('message', 'system');
613
+
614
+ const contentDiv = document.createElement('div');
615
+ contentDiv.classList.add('message-content');
616
+ contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
617
+ contentDiv.style.color = 'var(--error-color)';
618
+
619
+ const errorP = document.createElement('p');
620
+ errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`;
621
+ contentDiv.appendChild(errorP);
622
+
623
+ messageDiv.appendChild(contentDiv);
624
+ chatMessages.appendChild(messageDiv);
625
+
626
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
627
+ chatMessages.scrollTop = chatMessages.scrollHeight;
628
+ }
629
+
630
+ /**
631
+ * textarea ๋†’์ด ์ž๋™ ์กฐ์ • ํ•จ์ˆ˜
632
+ */
633
+ function adjustTextareaHeight() {
634
+ userInput.style.height = 'auto';
635
+ userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
636
+ }
app/templates/index.html CHANGED
@@ -6,7 +6,6 @@
6
  <title>RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡</title>
7
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
8
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
9
- <link rel="stylesheet" href="{{ url_for('static', filename='css/device-style.css') }}">
10
  </head>
11
  <body>
12
  <div class="container">
@@ -29,7 +28,6 @@
29
  <div class="tabs">
30
  <button id="chatTab" class="tab active">๋Œ€ํ™”</button>
31
  <button id="docsTab" class="tab">๋ฌธ์„œ๊ด€๋ฆฌ</button>
32
- <button id="deviceTab" class="tab">์žฅ์น˜๊ด€๋ฆฌ</button>
33
  </div>
34
  </header>
35
 
@@ -121,51 +119,6 @@
121
  </div>
122
  </div>
123
  </section>
124
-
125
- <!-- ์žฅ์น˜๊ด€๋ฆฌ ํƒญ -->
126
- <section id="deviceSection" class="tab-content">
127
- <div class="device-container">
128
- <div class="device-section">
129
- <div class="device-toolbar">
130
- <h2>์žฅ์น˜ ์ƒํƒœ</h2>
131
- <button id="deviceRefreshButton" class="refresh-device-btn">
132
- <i class="fas fa-sync-alt"></i> ์ƒˆ๋กœ๊ณ ์นจ
133
- </button>
134
- </div>
135
-
136
- <div id="deviceStatus">
137
- <!-- ์žฅ์น˜ ์„œ๋ฒ„ ์ƒํƒœ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค -->
138
- <div class="loading-device">
139
- <div class="spinner"></div>
140
- <p>์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘...</p>
141
- </div>
142
- </div>
143
-
144
- <div id="deviceList">
145
- <!-- ์žฅ์น˜ ๋ชฉ๋ก์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค -->
146
- </div>
147
- </div>
148
-
149
- <div class="programs-section">
150
- <div class="device-toolbar">
151
- <h2>ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰</h2>
152
- <button id="loadProgramsButton" class="load-programs-btn">
153
- <i class="fas fa-list"></i> ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ
154
- </button>
155
- </div>
156
-
157
- <div id="programsContainer" class="programs-container">
158
- <div id="programsList">
159
- <!-- ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค -->
160
- <div class="loading-programs">
161
- <div class="spinner"></div>
162
- <p>ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์ค‘...</p>
163
- </div>
164
- </div>
165
- </div>
166
- </div>
167
- </div>
168
- </section>
169
  </main>
170
 
171
  <footer>
 
6
  <title>RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡</title>
7
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
8
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
 
9
  </head>
10
  <body>
11
  <div class="container">
 
28
  <div class="tabs">
29
  <button id="chatTab" class="tab active">๋Œ€ํ™”</button>
30
  <button id="docsTab" class="tab">๋ฌธ์„œ๊ด€๋ฆฌ</button>
 
31
  </div>
32
  </header>
33
 
 
119
  </div>
120
  </div>
121
  </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </main>
123
 
124
  <footer>