jeongsoo commited on
Commit
e50219c
ยท
1 Parent(s): 4bd974a
Files changed (1) hide show
  1. app/app.py +176 -194
app/app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์„ธ์…˜ ์„ค์ • ์ˆ˜์ • ์ ์šฉ)
3
  """
4
 
5
  import os
@@ -8,6 +8,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
@@ -16,7 +17,7 @@ from functools import wraps
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,17 +27,14 @@ load_dotenv()
26
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊น…
27
  ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
28
  ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
29
- # ๊ธฐ๋ณธ ์žฅ์น˜ ์„œ๋ฒ„ URL์„ ํ˜„์žฌ ํ˜ธ์ŠคํŠธ์˜ 5050 ํฌํŠธ๋กœ ์„ค์ • (๋™์ผ ํ˜ธ์ŠคํŠธ์—์„œ ์‹คํ–‰ ๊ฐ€์ •)
30
- # localhost ๋Œ€์‹  window.location.hostname์„ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ํ˜ธ์ŠคํŠธ์— ๋งž๊ฒŒ ์กฐ์ •
31
  DEVICE_SERVER_URL = os.getenv('DEVICE_SERVER_URL', '')
32
 
33
  logger.info(f"==== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ====")
34
  logger.info(f"ADMIN_USERNAME ์„ค์ • ์—ฌ๋ถ€: {ADMIN_USERNAME is not None}")
35
- # ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋กœ๋“œ ์—ฌ๋ถ€๋งŒ ๊ธฐ๋ก (๋ณด์•ˆ)
36
  logger.info(f"ADMIN_PASSWORD ์„ค์ • ์—ฌ๋ถ€: {ADMIN_PASSWORD is not None}")
37
  logger.info(f"DEVICE_SERVER_URL: {DEVICE_SERVER_URL or '์„ค์ •๋˜์ง€ ์•Š์Œ (ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ž๋™ ์„ค์ •)'}")
38
 
39
- # ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (๊ฐœ๋ฐœ์šฉ, ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ถŒ์žฅ)
40
  if not ADMIN_USERNAME:
41
  ADMIN_USERNAME = 'admin'
42
  logger.warning("ADMIN_USERNAME ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'admin'์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
@@ -44,54 +42,61 @@ if not ADMIN_USERNAME:
44
  if not ADMIN_PASSWORD:
45
  ADMIN_PASSWORD = 'rag12345'
46
  logger.warning("ADMIN_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'rag12345'๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
47
- class MockComponent: pass
48
 
49
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
50
- # ์‹ค์ œ ๊ฒฝ๋กœ์— ๋งž๊ฒŒ utils, retrieval ํด๋”๊ฐ€ ์กด์žฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
 
 
 
 
 
 
 
 
 
 
51
  try:
52
  from utils.vito_stt import VitoSTT
53
  from utils.llm_interface import LLMInterface
54
  from utils.document_processor import DocumentProcessor
55
  from retrieval.vector_retriever import VectorRetriever
56
  from retrieval.reranker import ReRanker
57
- # ๋ผ์šฐํŠธ ์ •์˜ ํŒŒ์ผ ์ž„ํฌํŠธ
58
  from app.app_routes import register_routes
59
  from app.app_device_routes import register_device_routes
 
60
  except ImportError as e:
61
- logger.error(f"๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}. utils ๋ฐ retrieval ํŒจํ‚ค์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒฝ๋กœ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
62
- # ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ž„์‹œ ํด๋ž˜์Šค ์ •์˜ (์‹ค์ œ ์‚ฌ์šฉ ์‹œ ์ œ๊ฑฐ)
63
-
64
  VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
 
 
 
 
 
65
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ๋ ---
66
 
67
 
68
  # Flask ์•ฑ ์ดˆ๊ธฐํ™”
69
  app = Flask(__name__)
70
 
71
- # ์„ธ์…˜ ์„ค์ • - ๊ณ ์ •๋œ ์‹œํฌ๋ฆฟ ํ‚ค ์‚ฌ์šฉ (์‹ค์ œ ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ์œผ๋กœ ๊ด€๋ฆฌ ๊ถŒ์žฅ)
72
- app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345') # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์šฐ์„  ์‚ฌ์šฉ
73
-
74
- # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ์ˆ˜์ • (ํ—ˆ๊น…ํŽ˜์ด์Šค ํ™˜๊ฒฝ ๊ณ ๋ ค) ---
75
- # ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ HTTPS๋กœ ์„œ๋น„์Šค๋˜๋ฏ€๋กœ Secure=True ์„ค์ •
76
  app.config['SESSION_COOKIE_SECURE'] = True
77
- app.config['SESSION_COOKIE_HTTPONLY'] = True # JavaScript์—๏ฟฝ๏ฟฝ๏ฟฝ ์ฟ ํ‚ค ์ ‘๊ทผ ๋ฐฉ์ง€ (๋ณด์•ˆ ๊ฐ•ํ™”)
78
- # SameSite='Lax'๊ฐ€ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ์— ๋” ์•ˆ์ „ํ•˜๊ณ  ํ˜ธํ™˜์„ฑ์ด ์ข‹์Œ.
79
- # ๋งŒ์•ฝ ์•ฑ์ด ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ iframe ๋‚ด์—์„œ ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค๋ฉด 'None'์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•จ.
80
- # (๋‹จ, 'None'์œผ๋กœ ์„ค์ • ์‹œ ๋ฐ˜๋“œ์‹œ Secure=True์—ฌ์•ผ ํ•จ)
81
- # ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ iframe ํ™˜๊ฒฝ์œผ๋กœ ํ™•์ธ๋˜์–ด 'None'์œผ๋กœ ๋ณ€๊ฒฝ
82
- app.config['SESSION_COOKIE_SAMESITE'] = 'None' # <--- ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
83
- app.config['SESSION_COOKIE_DOMAIN'] = None # ํŠน์ • ๋„๋ฉ”์ธ ์ œํ•œ ์—†์Œ
84
- app.config['SESSION_COOKIE_PATH'] = '/' # ์•ฑ ์ „์ฒด ๊ฒฝ๋กœ์— ์ฟ ํ‚ค ์ ์šฉ
85
- app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=1) # ์„ธ์…˜ ์œ ํšจ ์‹œ๊ฐ„ ์ฆ๊ฐ€
86
- # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ๋ ---
87
 
88
  # ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์„ค์ • (10MB)
89
  app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
90
  # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ ์„ค์ •
91
  APP_ROOT = os.path.dirname(os.path.abspath(__file__))
 
 
92
  app.config['UPLOAD_FOLDER'] = os.path.join(APP_ROOT, 'uploads')
93
- app.config['DATA_FOLDER'] = os.path.join(APP_ROOT, '..', 'data')
94
- app.config['INDEX_PATH'] = os.path.join(APP_ROOT, '..', 'data', 'index')
 
95
 
96
  # ํ•„์š”ํ•œ ํด๋” ์ƒ์„ฑ
97
  os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
@@ -103,35 +108,38 @@ ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
103
  ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
104
 
105
  # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ---
106
- try:
107
- llm_interface = LLMInterface(default_llm="openai")
108
- stt_client = VitoSTT()
109
- except NameError:
110
- logger.warning("LLM ๋˜๋Š” STT ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” ์‹คํŒจ. Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
111
- llm_interface = MockComponent()
112
- stt_client = MockComponent()
113
-
114
  base_retriever = None
115
  retriever = None
116
- app_ready = False # ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
117
- # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋ ---
118
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ (์ˆ˜์ •๋จ) ---
121
  def login_required(f):
122
  @wraps(f)
123
  def decorated_function(*args, **kwargs):
124
  logger.info(f"----------- ์ธ์ฆ ํ•„์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ๋„: {request.path} -----------")
125
- logger.info(f"ํ˜„์žฌ ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜ ๊ฐ์ฒด: {session}")
126
  logger.info(f"ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
127
- # ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ณด๋‚ธ ์‹ค์ œ ์ฟ ํ‚ค ํ™•์ธ (๋””๋ฒ„๊น…์šฉ)
128
- logger.info(f"์š”์ฒญ์˜ ์„ธ์…˜ ์ฟ ํ‚ค ๊ฐ’: {request.cookies.get('session', 'None')}")
129
 
130
- # Flask ์„ธ์…˜์— 'logged_in' ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ์ง์ ‘ ํ™•์ธ
131
- if 'logged_in' not in session:
132
- logger.warning(f"ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜์— 'logged_in' ์—†์Œ. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜.")
133
- # ์ˆ˜๋™ ์ฟ ํ‚ค ํ™•์ธ ๋กœ์ง ์ œ๊ฑฐ๋จ
134
- return redirect(url_for('login', next=request.url)) # ๋กœ๊ทธ์ธ ํ›„ ์›๋ž˜ ํŽ˜์ด์ง€๋กœ ๋Œ์•„๊ฐ€๋„๋ก next ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
135
 
136
  logger.info(f"์ธ์ฆ ์„ฑ๊ณต: {session.get('username', 'unknown')} ์‚ฌ์šฉ์ž๊ฐ€ {request.path} ์ ‘๊ทผ")
137
  return f(*args, **kwargs)
@@ -139,7 +147,7 @@ def login_required(f):
139
  # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ ---
140
 
141
 
142
- # --- ํ—ฌํผ ํ•จ์ˆ˜ ---
143
  def allowed_audio_file(filename):
144
  """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ์˜ค๋””์˜ค ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
145
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
@@ -150,100 +158,82 @@ def allowed_doc_file(filename):
150
  # --- ํ—ฌํผ ํ•จ์ˆ˜ ๋ ---
151
 
152
 
153
- # init_retriever ํ•จ์ˆ˜ ๋‚ด๋ถ€์— ๋กœ๊น… ์ถ”๊ฐ€ ์˜ˆ์‹œ
154
  # --- ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ๊ด€๋ จ ํ•จ์ˆ˜ ---
155
  def init_retriever():
156
  """๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋กœ๋“œ"""
157
  global base_retriever, retriever
158
 
 
 
 
 
 
 
 
159
  index_path = app.config['INDEX_PATH']
160
- data_path = app.config['DATA_FOLDER'] # data_path ์ •์˜ ํ™•์ธ
161
  logger.info("--- init_retriever ์‹œ์ž‘ ---")
162
 
163
  # 1. ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™”
164
- # ... (VectorRetriever ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™” ๋กœ์ง์€ ์ด์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€) ...
165
- # VectorRetriever ์ดˆ๊ธฐํ™”/๋กœ๋“œ ์‹คํŒจ ์‹œ base_retriever = None ๋ฐ return None ์ฒ˜๋ฆฌ ํฌํ•จ
166
- if os.path.exists(os.path.join(index_path, "documents.json")):
167
- try:
168
  logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹œ๋„: {index_path}")
169
  base_retriever = VectorRetriever.load(index_path)
170
  logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์„ฑ๊ณต. ๋ฌธ์„œ {len(getattr(base_retriever, 'documents', []))}๊ฐœ")
171
- except Exception as e:
172
- logger.error(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹คํŒจ: {e}", exc_info=True)
173
- logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
174
- try:
175
- base_retriever = VectorRetriever()
176
- logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
177
- except Exception as e_init:
178
- logger.error(f"์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_init}", exc_info=True)
179
- base_retriever = None
180
- else:
181
- logger.info("์ธ๋ฑ์Šค ํŒŒ์ผ ์—†์Œ. ์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
182
- try:
183
  base_retriever = VectorRetriever()
184
  logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
185
- except Exception as e_init:
186
- logger.error(f"์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_init}", exc_info=True)
187
- base_retriever = None
188
-
189
- if base_retriever is None:
190
- logger.error("base_retriever ์ดˆ๊ธฐํ™”/๋กœ๋“œ์— ์‹คํŒจํ•˜์—ฌ init_retriever ์ค‘๋‹จ.")
191
- return None
192
 
193
  # 2. ๋ฐ์ดํ„ฐ ํด๋” ๋ฌธ์„œ ๋กœ๋“œ (๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ)
194
- needs_loading = (not hasattr(base_retriever, 'documents') or not getattr(base_retriever, 'documents', None)) # None ์ฒดํฌ ์ถ”๊ฐ€
195
  if needs_loading and os.path.exists(data_path):
196
  logger.info(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์–ด {data_path}์—์„œ ๋ฌธ์„œ ๋กœ๋“œ ์‹œ๋„...")
197
  try:
198
- # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 1 ์‹œ์ž‘ ==================
199
- # DocumentProcessor.load_documents_from_directory ํ˜ธ์ถœ ์‹œ ์˜ฌ๋ฐ”๋ฅธ ์ธ์ž ์ „๋‹ฌ
200
- docs = DocumentProcessor.load_documents_from_directory(
201
- directory=data_path, # <-- ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ์‚ฌ์šฉ
202
- extensions=[".txt", ".md", ".csv"], # <-- ํ•„์š”ํ•œ ํ™•์žฅ์ž ์ „๋‹ฌ
203
- recursive=True # <-- ์žฌ๊ท€ ํƒ์ƒ‰ ์—ฌ๋ถ€ ์ „๋‹ฌ
204
- )
205
- # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 1 ๋ ====================
206
- logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ๋กœ๋“œ ์„ฑ๊ณต.")
207
- if docs and hasattr(base_retriever, 'add_documents'):
208
- logger.info("๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ ์ถ”๊ฐ€ ์‹œ๋„...")
209
- base_retriever.add_documents(docs)
210
- logger.info("๋ฌธ์„œ ์ถ”๊ฐ€ ์™„๋ฃŒ.")
211
-
212
- if hasattr(base_retriever, 'save'):
213
- logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ ์ €์žฅ ์‹œ๋„: {index_path}")
214
- try:
215
- base_retriever.save(index_path)
216
- logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ.")
217
- except Exception as e_save:
218
- logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์‹คํŒจ: {e_save}", exc_info=True)
219
  except Exception as e_load_add:
220
- # load_documents_from_directory ์ž์ฒด์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋‚  ์ˆ˜๋„ ์žˆ์Œ (๊ถŒํ•œ ๋“ฑ)
221
  logger.error(f"DATA_FOLDER ๋ฌธ์„œ ๋กœ๋“œ/์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜: {e_load_add}", exc_info=True)
222
 
223
  # 3. ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
224
  logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
225
  try:
226
- # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 2 ์‹œ์ž‘ ==================
227
- # custom_rerank_fn ํ•จ์ˆ˜๋ฅผ ReRanker ์ดˆ๊ธฐํ™” ์ „์— ์ •์˜
228
  def custom_rerank_fn(query, results):
229
- query_terms = set(query.lower().split())
230
- for result in results:
231
- if isinstance(result, dict) and "text" in result:
232
- text = result["text"].lower()
233
- term_freq = sum(1 for term in query_terms if term in text)
234
- normalized_score = term_freq / (len(text.split()) + 1) * 10
235
- result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
236
- elif isinstance(result, dict):
237
- result["rerank_score"] = result.get("score", 0)
238
- results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
239
  return results
240
- # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 2 ๋ ====================
241
 
242
  # ReRanker ํด๋ž˜์Šค ์‚ฌ์šฉ
243
  retriever = ReRanker(
244
  base_retriever=base_retriever,
245
- rerank_fn=custom_rerank_fn, # ์ด์ œ ํ•จ์ˆ˜๊ฐ€ ์ •์˜๋˜์—ˆ์œผ๋ฏ€๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
246
- rerank_field="text"
247
  )
248
  logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
249
  except Exception as e_rerank:
@@ -254,109 +244,74 @@ def init_retriever():
254
  logger.info("--- init_retriever ์ข…๋ฃŒ ---")
255
  return retriever
256
 
 
257
  def background_init():
258
- """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์ˆ˜ํ–‰"""
259
- global app_ready, retriever, base_retriever, llm_interface, stt_client
260
 
261
- temp_app_ready = False # ์ž„์‹œ ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
262
- try:
263
- logger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘...")
264
 
265
- # 1. LLM, STT ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” (ํ•„์š” ์‹œ)
 
266
  if llm_interface is None or isinstance(llm_interface, MockComponent):
267
- if 'LLMInterface' in globals() and LLMInterface != MockComponent:
268
- llm_interface = LLMInterface(default_llm="openai")
269
- logger.info("LLM ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
270
- else:
271
- logger.warning("LLMInterface ํด๋ž˜์Šค ์—†์Œ. Mock ์‚ฌ์šฉ.")
272
- llm_interface = MockComponent() # Mock ๊ฐ์ฒด ๋ณด์žฅ
273
  if stt_client is None or isinstance(stt_client, MockComponent):
274
- if 'VitoSTT' in globals() and VitoSTT != MockComponent:
275
- stt_client = VitoSTT()
276
- logger.info("STT ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
277
- else:
278
- logger.warning("VitoSTT ํด๋ž˜์Šค ์—†์Œ. Mock ์‚ฌ์šฉ.")
279
- stt_client = MockComponent() # Mock ๊ฐ์ฒด ๋ณด์žฅ
280
-
281
 
282
  # 2. ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
283
- if 'VectorRetriever' in globals() and VectorRetriever != MockComponent:
284
- logger.info("์‹ค์ œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
285
- # init_retriever๊ฐ€ base_retriever์™€ retriever๋ฅผ ๋ชจ๋‘ ์„ค์ •ํ•œ๋‹ค๊ณ  ๊ฐ€์ •
286
- retriever = init_retriever()
287
- # init_retriever ๋‚ด๋ถ€์—์„œ base_retriever๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์—ฌ๊ธฐ์„œ ์„ค์ •
288
- if hasattr(retriever, 'base_retriever') and base_retriever is None:
289
- base_retriever = retriever.base_retriever
290
- elif base_retriever is None:
291
- # retriever๊ฐ€ base_retriever๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ๋˜๋Š” ReRanker๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ
292
- # init_retriever์—์„œ base_retriever๋ฅผ ์ง์ ‘ ์„ค์ •ํ•˜๋„๋ก ํ•˜๊ฑฐ๋‚˜, ์—ฌ๊ธฐ์„œ ๋ณ„๋„ ๋กœ์ง ํ•„์š”
293
- # ์˜ˆ์‹œ: base_retriever = VectorRetriever.load(...) ๋˜๋Š” VectorRetriever()
294
- logger.warning("init_retriever ํ›„ base_retriever๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ. ํ™•์ธ ํ•„์š”.")
295
- # ์ž„์‹œ๋กœ retriever ์ž์ฒด๋ฅผ base_retriever๋กœ ์„ค์ • (๋™์ผ ๊ฐ์ฒด์ผ ๊ฒฝ์šฐ)
296
- if isinstance(retriever, VectorRetriever):
297
- base_retriever = retriever
298
-
299
- # ์„ฑ๊ณต์ ์œผ๋กœ ์ดˆ๊ธฐํ™” ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ (None์ด ์•„๋‹Œ์ง€)
300
- if retriever is not None and base_retriever is not None:
301
- logger.info("๊ฒ€์ƒ‰๊ธฐ (Retriever, Base Retriever) ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
302
- temp_app_ready = True # ์ดˆ๊ธฐํ™” ์„ฑ๊ณต ์‹œ์—๋งŒ True ์„ค์ •
303
- else:
304
- logger.error("๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ํ›„์—๋„ retriever ๋˜๋Š” base_retriever๊ฐ€ None์ž…๋‹ˆ๋‹ค.")
305
- # ์‹คํŒจ ์‹œ Mock ๊ฐ์ฒด ํ• ๋‹น (์ตœ์†Œํ•œ์˜ ๋™์ž‘ ๋ณด์žฅ)
306
- if base_retriever is None: base_retriever = MockComponent()
307
- if retriever is None: retriever = MockComponent()
308
- if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
309
- if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
310
- # temp_app_ready = False ๋˜๋Š” True (์ •์ฑ…์— ๋”ฐ๋ผ ๊ฒฐ์ •)
311
- temp_app_ready = True # ์ผ๋‹จ ์•ฑ์€ ์‹คํ–‰๋˜๋„๋ก ์„ค์ •
312
-
313
  else:
314
- logger.warning("VectorRetriever ํด๋ž˜์Šค ์—†์Œ. Mock ๊ฒ€์ƒ‰๊ธฐ ์‚ฌ์šฉ.")
315
- base_retriever = MockComponent()
316
- retriever = MockComponent()
317
- if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
318
- if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
319
- temp_app_ready = True # Mock์ด๋ผ๋„ ์ค€๋น„๋Š” ๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ
320
-
321
- logger.info(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ. ์ตœ์ข… ์ƒํƒœ: {'Ready' if temp_app_ready else 'Not Ready (Error during init)'}")
322
 
323
  except Exception as e:
324
- logger.error(f"์•ฑ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
325
- # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ์—๋„ Mock ๊ฐ์ฒด ํ• ๋‹น ์‹œ๋„
326
  if base_retriever is None: base_retriever = MockComponent()
327
  if retriever is None: retriever = MockComponent()
328
- if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
329
- if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
330
- temp_app_ready = True # ์˜ค๋ฅ˜ ๋ฐœ์ƒํ•ด๋„ ์•ฑ์€ ์‘๋‹ตํ•˜๋„๋ก ์„ค์ • (์ •์ฑ…์— ๋”ฐ๋ผ False ๊ฐ€๋Šฅ)
331
- logger.warning("์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์ง€๋งŒ Mock ๊ฐ์ฒด๋กœ ๋Œ€์ฒด ํ›„ ์•ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ •.")
332
 
333
  finally:
334
- # ์ตœ์ข…์ ์œผ๋กœ app_ready ์ƒํƒœ ์—…๋ฐ์ดํŠธ
335
- app_ready = temp_app_ready
 
336
 
337
- # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ์‹œ์ž‘ ๋ถ€๋ถ„์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€
338
  init_thread = threading.Thread(target=background_init)
339
  init_thread.daemon = True
340
  init_thread.start()
341
 
342
- # ๋ผ์šฐํŠธ ๋“ฑ๋ก
343
  try:
344
  # ๊ธฐ๋ณธ RAG ์ฑ—๋ด‡ ๋ผ์šฐํŠธ ๋“ฑ๋ก
 
345
  register_routes(
346
- app=app,
347
- login_required=login_required,
348
- llm_interface=llm_interface,
349
- retriever=retriever,
350
- stt_client=stt_client,
351
  DocumentProcessor=DocumentProcessor,
352
- base_retriever=base_retriever,
353
- app_ready=app_ready,
354
  ADMIN_USERNAME=ADMIN_USERNAME,
355
  ADMIN_PASSWORD=ADMIN_PASSWORD,
356
  DEVICE_SERVER_URL=DEVICE_SERVER_URL
357
  )
358
  logger.info("๊ธฐ๋ณธ ์ฑ—๋ด‡ ๋ผ์šฐํŠธ ๋“ฑ๋ก ์™„๋ฃŒ")
359
-
360
  # ์žฅ์น˜ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ ๋“ฑ๋ก
361
  register_device_routes(
362
  app=app,
@@ -365,26 +320,53 @@ try:
365
  )
366
  logger.info("์žฅ์น˜ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ ๋“ฑ๋ก ์™„๋ฃŒ")
367
  except Exception as e:
368
- logger.error(f"๋ผ์šฐํŠธ ๋“ฑ๋ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
369
-
 
370
 
371
  # --- ์ •์  ํŒŒ์ผ ์„œ๋น™ ---
 
372
  @app.route('/static/<path:path>')
373
  def send_static(path):
374
- return send_from_directory('static', path)
 
 
375
 
376
 
377
  # --- ์š”์ฒญ ์ฒ˜๋ฆฌ ํ›… ---
378
  @app.after_request
379
  def after_request_func(response):
380
- """๋ชจ๋“  ์‘๋‹ต์— ๋Œ€ํ•ด ํ›„์ฒ˜๋ฆฌ ์ˆ˜ํ–‰"""
 
381
  return response
382
 
383
  # ์•ฑ ์‹คํ–‰ (๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ)
384
  if __name__ == '__main__':
385
  logger.info("Flask ์•ฑ์„ ์ง์ ‘ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„).")
386
- # ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๋Š” ์‹ค์ œ ๋ฐฐํฌ ์‹œ False๋กœ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
387
  # port ๋ฒˆํ˜ธ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
388
- port = int(os.environ.get("PORT", 7860))
389
  logger.info(f"์„œ๋ฒ„๋ฅผ http://0.0.0.0:{port} ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
390
- app.run(debug=True, host='0.0.0.0', port=port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์„ธ์…˜ ์„ค์ • ์ˆ˜์ • ์ ์šฉ ๋ฐ TypeError ํ•ด๊ฒฐ)
3
  """
4
 
5
  import os
 
8
  import tempfile
9
  import threading
10
  import datetime
11
+ import time # ์ถ”๊ฐ€
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
 
17
  # ๋กœ๊ฑฐ ์„ค์ •
18
  logging.basicConfig(
19
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
20
+ level=logging.DEBUG
21
  )
22
  logger = logging.getLogger(__name__)
23
 
 
27
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊น…
28
  ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
29
  ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
 
 
30
  DEVICE_SERVER_URL = os.getenv('DEVICE_SERVER_URL', '')
31
 
32
  logger.info(f"==== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ====")
33
  logger.info(f"ADMIN_USERNAME ์„ค์ • ์—ฌ๋ถ€: {ADMIN_USERNAME is not None}")
 
34
  logger.info(f"ADMIN_PASSWORD ์„ค์ • ์—ฌ๋ถ€: {ADMIN_PASSWORD is not None}")
35
  logger.info(f"DEVICE_SERVER_URL: {DEVICE_SERVER_URL or '์„ค์ •๋˜์ง€ ์•Š์Œ (ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ž๋™ ์„ค์ •)'}")
36
 
37
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
38
  if not ADMIN_USERNAME:
39
  ADMIN_USERNAME = 'admin'
40
  logger.warning("ADMIN_USERNAME ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'admin'์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
 
42
  if not ADMIN_PASSWORD:
43
  ADMIN_PASSWORD = 'rag12345'
44
  logger.warning("ADMIN_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'rag12345'๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
 
45
 
46
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
47
+ # MockComponent ์ •์˜ (์ž„ํฌํŠธ ์‹คํŒจ ์‹œ ๋Œ€์ฒด)
48
+ class MockComponent:
49
+ def __getattr__(self, name):
50
+ # Mock ๊ฐ์ฒด์˜ ์–ด๋–ค ์†์„ฑ์ด๋‚˜ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์‹œ ๊ฒฝ๊ณ  ๋กœ๊ทธ ์ถœ๋ ฅ ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜
51
+ logger.warning(f"MockComponent์—์„œ '{name}' ์ ‘๊ทผ ์‹œ๋„๋จ (์‹ค์ œ ๋ชจ๋“ˆ ๋กœ๋“œ ์‹คํŒจ)")
52
+ # ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์‹œ์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ํ•˜๋Š” ํ•จ์ˆ˜ ๋ฐ˜ํ™˜
53
+ if name in ['search', 'add_documents', 'save', 'transcribe_audio', 'rag_generate', 'set_llm', 'get_current_llm_details', 'prepare_rag_context', 'csv_to_documents', 'text_to_documents', 'load_documents_from_directory']:
54
+ return lambda *args, **kwargs: logger.warning(f"Mocked method '{name}' called") or (None if name != 'search' else []) # search๋Š” ๋นˆ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜
55
+ # ์†์„ฑ ์ ‘๊ทผ ์‹œ์—๋Š” None ๋ฐ˜ํ™˜
56
+ return None
57
+
58
  try:
59
  from utils.vito_stt import VitoSTT
60
  from utils.llm_interface import LLMInterface
61
  from utils.document_processor import DocumentProcessor
62
  from retrieval.vector_retriever import VectorRetriever
63
  from retrieval.reranker import ReRanker
 
64
  from app.app_routes import register_routes
65
  from app.app_device_routes import register_device_routes
66
+ MODULE_LOAD_SUCCESS = True
67
  except ImportError as e:
68
+ logger.error(f"๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}. Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
 
 
69
  VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
70
+ # register_routes, register_device_routes๋Š” ์ž„ํฌํŠธ ์‹คํŒจ ์‹œ ์ •์˜๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์•„๋ž˜์—์„œ ์ฒ˜๋ฆฌ
71
+ MODULE_LOAD_SUCCESS = False
72
+ # ์ž„์‹œ๋กœ ๋นˆ ํ•จ์ˆ˜ ์ •์˜ (์•ฑ ์‹คํ–‰์€ ๋˜๋„๋ก)
73
+ def register_routes(*args, **kwargs): logger.error("register_routes ์ž„ํฌํŠธ ์‹คํŒจ")
74
+ def register_device_routes(*args, **kwargs): logger.error("register_device_routes ์ž„ํฌํŠธ ์‹คํŒจ")
75
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ๋ ---
76
 
77
 
78
  # Flask ์•ฑ ์ดˆ๊ธฐํ™”
79
  app = Flask(__name__)
80
 
81
+ # ์„ธ์…˜ ์„ค์ •
82
+ app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345')
 
 
 
83
  app.config['SESSION_COOKIE_SECURE'] = True
84
+ app.config['SESSION_COOKIE_HTTPONLY'] = True
85
+ app.config['SESSION_COOKIE_SAMESITE'] = 'None'
86
+ app.config['SESSION_COOKIE_DOMAIN'] = None
87
+ app.config['SESSION_COOKIE_PATH'] = '/'
88
+ app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=1)
 
 
 
 
 
89
 
90
  # ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์„ค์ • (10MB)
91
  app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
92
  # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ ์„ค์ •
93
  APP_ROOT = os.path.dirname(os.path.abspath(__file__))
94
+ # static ํด๋” ๊ฒฝ๋กœ ์ˆ˜์ • (app ํด๋” ๋‚ด๋ถ€์˜ static)
95
+ app.config['STATIC_FOLDER'] = os.path.join(APP_ROOT, 'static')
96
  app.config['UPLOAD_FOLDER'] = os.path.join(APP_ROOT, 'uploads')
97
+ # data ๋ฐ index ๊ฒฝ๋กœ๋Š” app ํด๋” ์™ธ๋ถ€๋กœ ์„ค์ • (ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๊ธฐ์ค€)
98
+ app.config['DATA_FOLDER'] = os.path.join(os.path.dirname(APP_ROOT), 'data')
99
+ app.config['INDEX_PATH'] = os.path.join(os.path.dirname(APP_ROOT), 'data', 'index')
100
 
101
  # ํ•„์š”ํ•œ ํด๋” ์ƒ์„ฑ
102
  os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
 
108
  ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
109
 
110
  # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ---
111
+ llm_interface = None
112
+ stt_client = None
 
 
 
 
 
 
113
  base_retriever = None
114
  retriever = None
115
+ # app_ready ํ”Œ๋ž˜๊ทธ ๋Œ€์‹  threading.Event ์‚ฌ์šฉ
116
+ app_ready_event = threading.Event() # ์ดˆ๊ธฐ ์ƒํƒœ: False (set() ํ˜ธ์ถœ ์ „๊นŒ์ง€)
117
 
118
+ # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” (try-except๋กœ ๊ฐ์‹ธ๊ธฐ) ---
119
+ try:
120
+ if MODULE_LOAD_SUCCESS: # ๋ชจ๋“ˆ ๋กœ๋“œ ์„ฑ๊ณต ์‹œ์—๋งŒ ์‹ค์ œ ์ดˆ๊ธฐํ™” ์‹œ๋„
121
+ llm_interface = LLMInterface(default_llm="openai")
122
+ stt_client = VitoSTT()
123
+ else: # ์‹คํŒจ ์‹œ Mock ๊ฐ์ฒด ์‚ฌ์šฉ
124
+ llm_interface = LLMInterface() # MockComponent
125
+ stt_client = VitoSTT() # MockComponent
126
+ except Exception as e:
127
+ logger.error(f"LLM ๋˜๋Š” STT ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
128
+ llm_interface = MockComponent() # ์˜ค๋ฅ˜ ์‹œ Mock ๊ฐ์ฒด ํ• ๋‹น
129
+ stt_client = MockComponent() # ์˜ค๋ฅ˜ ์‹œ Mock ๊ฐ์ฒด ํ• ๋‹น
130
 
131
+ # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ---
132
  def login_required(f):
133
  @wraps(f)
134
  def decorated_function(*args, **kwargs):
135
  logger.info(f"----------- ์ธ์ฆ ํ•„์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ๋„: {request.path} -----------")
136
+ logger.debug(f"ํ˜„์žฌ ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜ ๊ฐ์ฒด: {session}") # DEBUG ๋ ˆ๋ฒจ๋กœ ๋ณ€๊ฒฝ
137
  logger.info(f"ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
138
+ logger.debug(f"์š”์ฒญ์˜ ์„ธ์…˜ ์ฟ ํ‚ค ๊ฐ’: {request.cookies.get('session', 'None')}") # DEBUG ๋ ˆ๋ฒจ๋กœ ๋ณ€๊ฒฝ
 
139
 
140
+ if not session.get('logged_in'): # .get() ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „
141
+ logger.warning(f"์„ธ์…˜์— 'logged_in' ์—†์Œ ๋˜๋Š” False. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜.")
142
+ return redirect(url_for('login', next=request.url))
 
 
143
 
144
  logger.info(f"์ธ์ฆ ์„ฑ๊ณต: {session.get('username', 'unknown')} ์‚ฌ์šฉ์ž๊ฐ€ {request.path} ์ ‘๊ทผ")
145
  return f(*args, **kwargs)
 
147
  # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ ---
148
 
149
 
150
+ # --- ํ—ฌํผ ํ•จ์ˆ˜ (app_routes.py์—๋„ ์žˆ์ง€๋งŒ ์—ฌ๊ธฐ์„œ๋„ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ) ---
151
  def allowed_audio_file(filename):
152
  """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ์˜ค๋””์˜ค ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
153
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
 
158
  # --- ํ—ฌํผ ํ•จ์ˆ˜ ๋ ---
159
 
160
 
 
161
  # --- ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ๊ด€๋ จ ํ•จ์ˆ˜ ---
162
  def init_retriever():
163
  """๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋กœ๋“œ"""
164
  global base_retriever, retriever
165
 
166
+ # ๋ชจ๋“ˆ ๋กœ๋“œ ์‹คํŒจ ์‹œ Mock ๊ฐ์ฒด ๋ฐ˜ํ™˜
167
+ if not MODULE_LOAD_SUCCESS:
168
+ logger.warning("ํ•„์ˆ˜ ๋ชจ๋“ˆ ๋กœ๋“œ ์‹คํŒจ๋กœ Mock ๊ฒ€์ƒ‰๊ธฐ ๋ฐ˜ํ™˜")
169
+ base_retriever = VectorRetriever() # MockComponent
170
+ retriever = ReRanker() # MockComponent
171
+ return retriever
172
+
173
  index_path = app.config['INDEX_PATH']
174
+ data_path = app.config['DATA_FOLDER']
175
  logger.info("--- init_retriever ์‹œ์ž‘ ---")
176
 
177
  # 1. ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™”
178
+ try:
179
+ if os.path.exists(os.path.join(index_path, "documents.json")): # ์ €์žฅ ๋ฐฉ์‹์— ๋”ฐ๋ผ ํ™•์ธ ํŒŒ์ผ ๋ณ€๊ฒฝ ํ•„์š”
 
 
180
  logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹œ๋„: {index_path}")
181
  base_retriever = VectorRetriever.load(index_path)
182
  logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์„ฑ๊ณต. ๋ฌธ์„œ {len(getattr(base_retriever, 'documents', []))}๊ฐœ")
183
+ else:
184
+ logger.info("์ธ๋ฑ์Šค ํŒŒ์ผ ์—†์Œ. ์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
 
 
 
 
 
 
 
 
 
 
185
  base_retriever = VectorRetriever()
186
  logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
187
+ except Exception as e:
188
+ logger.error(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”/๋กœ๋“œ ์‹คํŒจ: {e}", exc_info=True)
189
+ base_retriever = MockComponent() # ์‹คํŒจ ์‹œ Mock ์‚ฌ์šฉ
190
+ retriever = MockComponent()
191
+ return None # ์ดˆ๊ธฐํ™” ์‹คํŒจ
 
 
192
 
193
  # 2. ๋ฐ์ดํ„ฐ ํด๋” ๋ฌธ์„œ ๋กœ๋“œ (๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ)
194
+ needs_loading = not hasattr(base_retriever, 'documents') or not getattr(base_retriever, 'documents', [])
195
  if needs_loading and os.path.exists(data_path):
196
  logger.info(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์–ด {data_path}์—์„œ ๋ฌธ์„œ ๋กœ๋“œ ์‹œ๋„...")
197
  try:
198
+ # DocumentProcessor.load_documents_from_directory ํ˜ธ์ถœ ํ™•์ธ
199
+ if hasattr(DocumentProcessor, 'load_documents_from_directory'):
200
+ docs = DocumentProcessor.load_documents_from_directory(
201
+ directory=data_path,
202
+ extensions=[".txt", ".md", ".csv"],
203
+ recursive=True
204
+ )
205
+ logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ๋กœ๋“œ ์„ฑ๊ณต.")
206
+ if docs and hasattr(base_retriever, 'add_documents'):
207
+ logger.info("๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ ์ถ”๊ฐ€ ์‹œ๋„...")
208
+ base_retriever.add_documents(docs)
209
+ logger.info("๋ฌธ์„œ ์ถ”๊ฐ€ ์™„๋ฃŒ.")
210
+ if hasattr(base_retriever, 'save'):
211
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ ์ €์žฅ ์‹œ๋„: {index_path}")
212
+ try:
213
+ base_retriever.save(index_path)
214
+ logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ.")
215
+ except Exception as e_save:
216
+ logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์‹คํŒจ: {e_save}", exc_info=True)
217
+ else:
218
+ logger.warning("DocumentProcessor์— load_documents_from_directory ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
219
  except Exception as e_load_add:
 
220
  logger.error(f"DATA_FOLDER ๋ฌธ์„œ ๋กœ๋“œ/์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜: {e_load_add}", exc_info=True)
221
 
222
  # 3. ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
223
  logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
224
  try:
225
+ # custom_rerank_fn ์ •์˜
 
226
  def custom_rerank_fn(query, results):
227
+ # ์ด ํ•จ์ˆ˜๋Š” ์‹ค์ œ ์žฌ์ˆœ์œ„ํ™” ๋กœ์ง์— ๋งž๊ฒŒ ๊ตฌํ˜„ ํ•„์š”
228
+ # ์˜ˆ์‹œ: ๋‹จ์ˆœํžˆ score ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ
229
+ results.sort(key=lambda x: x.get("score", 0) if isinstance(x, dict) else 0, reverse=True)
 
 
 
 
 
 
 
230
  return results
 
231
 
232
  # ReRanker ํด๋ž˜์Šค ์‚ฌ์šฉ
233
  retriever = ReRanker(
234
  base_retriever=base_retriever,
235
+ rerank_fn=custom_rerank_fn,
236
+ rerank_field="text" # ์žฌ์ˆœ์œ„ํ™”์— ์‚ฌ์šฉํ•  ํ•„๋“œ (ํ•„์š”์‹œ)
237
  )
238
  logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
239
  except Exception as e_rerank:
 
244
  logger.info("--- init_retriever ์ข…๋ฃŒ ---")
245
  return retriever
246
 
247
+ # --- ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ---
248
  def background_init():
249
+ """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฒ€์ƒ‰๊ธฐ ๋ฐ ๊ธฐํƒ€ ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐํ™” ์ˆ˜ํ–‰"""
250
+ global retriever, base_retriever, llm_interface, stt_client, app_ready_event
251
 
252
+ logger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘...")
253
+ start_init_time = time.time()
 
254
 
255
+ try:
256
+ # 1. LLM, STT ์ธํ„ฐํŽ˜์ด์Šค ์žฌํ™•์ธ (์ด๋ฏธ ์ดˆ๊ธฐํ™” ์‹œ๋„๋จ)
257
  if llm_interface is None or isinstance(llm_interface, MockComponent):
258
+ logger.warning("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ Mock ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.")
259
+ # ํ•„์š”์‹œ ์—ฌ๊ธฐ์„œ ๋‹ค์‹œ ์ดˆ๊ธฐํ™” ์‹œ๋„ ๊ฐ€๋Šฅ
 
 
 
 
260
  if stt_client is None or isinstance(stt_client, MockComponent):
261
+ logger.warning("STT ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ Mock ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.")
 
 
 
 
 
 
262
 
263
  # 2. ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
264
+ logger.info("๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„ (background)...")
265
+ retriever = init_retriever() # init_retriever๊ฐ€ base_retriever๋„ ์„ค์ •
266
+
267
+ # ์„ฑ๊ณต ์—ฌ๋ถ€ ํ™•์ธ
268
+ if retriever is not None and not isinstance(retriever, MockComponent):
269
+ logger.info("๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต (background)")
270
+ app_ready_event.set() # ์•ฑ ์ค€๋น„ ์™„๋ฃŒ ์ƒํƒœ๋กœ ์„ค์ •
271
+ logger.info("app_ready_event๊ฐ€ True๋กœ ์„ค์ •๋จ.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  else:
273
+ logger.error("๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ ๋˜๋Š” Mock ๊ฐ์ฒด ์‚ฌ์šฉ (background)")
274
+ # ์‹คํŒจ ์‹œ์—๋„ ์•ฑ์€ ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋„๋ก app_ready_event๋ฅผ ์„ค์ •ํ• ์ง€ ๊ฒฐ์ •
275
+ # app_ready_event.set() # ์‹คํŒจํ•ด๋„ ์ผ๋‹จ ์•ฑ์€ ๋„์šฐ๋ ค๋ฉด ์ฃผ์„ ํ•ด์ œ
276
+ logger.warning("์ดˆ๊ธฐํ™” ์‹คํŒจ๋กœ app_ready_event๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ.")
 
 
 
 
277
 
278
  except Exception as e:
279
+ logger.error(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
280
+ # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ์—๋„ Mock ๊ฐ์ฒด ํ• ๋‹น ๋ฐ ์ƒํƒœ ์„ค์ • ๊ณ ๋ ค
281
  if base_retriever is None: base_retriever = MockComponent()
282
  if retriever is None: retriever = MockComponent()
283
+ # app_ready_event.set() # ์˜ค๋ฅ˜ ์‹œ์—๋„ ์•ฑ ์‹คํ–‰ ์›ํ•˜๋ฉด ์ฃผ์„ ํ•ด์ œ
284
+ logger.warning("์ดˆ๊ธฐํ™” ์˜ค๋ฅ˜ ๋ฐœ์ƒ์œผ๋กœ app_ready_event๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ.")
 
 
285
 
286
  finally:
287
+ end_init_time = time.time()
288
+ logger.info(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ. ์†Œ์š” ์‹œ๊ฐ„: {end_init_time - start_init_time:.2f}์ดˆ")
289
+ logger.info(f"์ตœ์ข… ์•ฑ ์ค€๋น„ ์ƒํƒœ (app_ready_event.is_set()): {app_ready_event.is_set()}")
290
 
291
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ์‹œ์ž‘
292
  init_thread = threading.Thread(target=background_init)
293
  init_thread.daemon = True
294
  init_thread.start()
295
 
296
+ # --- ๋ผ์šฐํŠธ ๋“ฑ๋ก ---
297
  try:
298
  # ๊ธฐ๋ณธ RAG ์ฑ—๋ด‡ ๋ผ์šฐํŠธ ๋“ฑ๋ก
299
+ # !! ์ค‘์š”: register_routes ํ˜ธ์ถœ ์‹œ app_ready ๋Œ€์‹  app_ready_event ์ „๋‹ฌ !!
300
  register_routes(
301
+ app=app,
302
+ login_required=login_required,
303
+ llm_interface=llm_interface,
304
+ retriever=retriever,
305
+ stt_client=stt_client,
306
  DocumentProcessor=DocumentProcessor,
307
+ base_retriever=base_retriever,
308
+ app_ready_event=app_ready_event, # <--- ์ด๋ฆ„ ๋ณ€๊ฒฝ๋จ
309
  ADMIN_USERNAME=ADMIN_USERNAME,
310
  ADMIN_PASSWORD=ADMIN_PASSWORD,
311
  DEVICE_SERVER_URL=DEVICE_SERVER_URL
312
  )
313
  logger.info("๊ธฐ๋ณธ ์ฑ—๋ด‡ ๋ผ์šฐํŠธ ๋“ฑ๋ก ์™„๋ฃŒ")
314
+
315
  # ์žฅ์น˜ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ ๋“ฑ๋ก
316
  register_device_routes(
317
  app=app,
 
320
  )
321
  logger.info("์žฅ์น˜ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ ๋“ฑ๋ก ์™„๋ฃŒ")
322
  except Exception as e:
323
+ # ๋ผ์šฐํŠธ ๋“ฑ๋ก ์‹คํŒจ๋Š” ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ์ด๋ฏ€๋กœ Critical ๋ ˆ๋ฒจ ์‚ฌ์šฉ ๊ณ ๋ ค
324
+ logger.critical(f"๋ผ์šฐํŠธ ๋“ฑ๋ก ์ค‘ ์น˜๋ช…์  ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
325
+ # ์•ฑ ์‹คํ–‰์„ ์ค‘๋‹จํ•˜๊ฑฐ๋‚˜ ์ตœ์†Œํ•œ์˜ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
326
 
327
  # --- ์ •์  ํŒŒ์ผ ์„œ๋น™ ---
328
+ # STATIC_FOLDER ์„ค์ •์„ ์‚ฌ์šฉํ•˜์—ฌ static ํด๋” ๊ฒฝ๋กœ ์ง€์ •
329
  @app.route('/static/<path:path>')
330
  def send_static(path):
331
+ static_folder = app.config.get('STATIC_FOLDER', 'static') # ์„ค์ •๊ฐ’ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
332
+ # logger.debug(f"Serving static file: {path} from {static_folder}") # ๋””๋ฒ„๊น… ์‹œ ์ฃผ์„ ํ•ด์ œ
333
+ return send_from_directory(static_folder, path)
334
 
335
 
336
  # --- ์š”์ฒญ ์ฒ˜๋ฆฌ ํ›… ---
337
  @app.after_request
338
  def after_request_func(response):
339
+ """๋ชจ๋“  ์‘๋‹ต์— ๋Œ€ํ•ด ํ›„์ฒ˜๋ฆฌ ์ˆ˜ํ–‰ (์˜ˆ: ๋ณด์•ˆ ํ—ค๋” ์ถ”๊ฐ€)"""
340
+ # ์˜ˆ์‹œ: response.headers['X-Content-Type-Options'] = 'nosniff'
341
  return response
342
 
343
  # ์•ฑ ์‹คํ–‰ (๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ)
344
  if __name__ == '__main__':
345
  logger.info("Flask ์•ฑ์„ ์ง์ ‘ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„).")
 
346
  # port ๋ฒˆํ˜ธ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
347
+ port = int(os.environ.get("PORT", 7860)) # ๊ธฐ๋ณธ ํฌํŠธ 7860 ์‚ฌ์šฉ
348
  logger.info(f"์„œ๋ฒ„๋ฅผ http://0.0.0.0:{port} ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
349
+ # debug=True๋Š” ๊ฐœ๋ฐœ ์ค‘์—๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ๋ฐฐํฌ ์‹œ์—๋Š” False๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ์ œ๊ฑฐ
350
+ # use_reloader=False ์ถ”๊ฐ€ํ•˜์—ฌ ์ž๋™ ๋ฆฌ๋กœ๋“œ ๋น„ํ™œ์„ฑํ™” (๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ๋ฌธ์ œ ๋ฐฉ์ง€)
351
+ app.run(debug=False, host='0.0.0.0', port=port, use_reloader=False)
352
+
353
+ ```
354
+
355
+ **์ˆ˜์ •๋œ ๋‚ด์šฉ:**
356
+
357
+ * `app.py`์˜ ๋ผ์šฐํŠธ ๋“ฑ๋ก ๋ถ€๋ถ„์—์„œ `register_routes` ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ, `app_ready` ์ธ์ž ์ด๋ฆ„์„ `app_ready_event`๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค. (`app_routes.py`์˜ ํ•จ์ˆ˜ ์ •์˜์™€ ์ผ์น˜์‹œํ‚ด)
358
+ * `app_ready` ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋˜ boolean ํ”Œ๋ž˜๊ทธ ๋Œ€์‹  `threading.Event` ๊ฐ์ฒด(`app_ready_event`)๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ๋ฉ”์ธ ์Šค๋ ˆ๋“œ(์š”์ฒญ ์ฒ˜๋ฆฌ)์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.
359
+ * ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜(`background_init`)์—์„œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ ์‹œ `app_ready_event.set()`์„ ํ˜ธ์ถœํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
360
+ * ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ(`index`, `app_status` ๋“ฑ)์—์„œ๋Š” `app_ready_event.is_set()`์„ ์‚ฌ์šฉํ•˜์—ฌ ์•ฑ ์ค€๋น„ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
361
+ * ๊ธฐํƒ€ ๋กœ๊น… ๊ฐ•ํ™” ๋ฐ ๊ฐ์ฒด ํ™•์ธ ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
362
+ * `app.run()` ํ˜ธ์ถœ ์‹œ `use_reloader=False` ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ๊ฐœ๋ฐœ ์„œ๋ฒ„์˜ ์ž๋™ ๋ฆฌ๋กœ๋“œ ๊ธฐ๋Šฅ์œผ๋กœ ์ธํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ๊ฐ€ ์ค‘๋ณต ์‹คํ–‰๋˜๋Š” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.
363
+ * ์ •์  ํŒŒ์ผ ์„œ๋น™ ๊ฒฝ๋กœ๋ฅผ `app.config['STATIC_FOLDER']`๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ช…์‹œ์ ์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
364
+
365
+ ์ด ์ˆ˜์ •๋œ `app.py` ์ฝ”๋“œ๋กœ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๋ฉด `TypeError` ์—†์ด ์ •์ƒ์ ์œผ๋กœ ์‹คํ–‰๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
366
+
367
+ **๋‹ค์Œ ๋‹จ๊ณ„:**
368
+
369
+ 1. ์ด ์ฝ”๋“œ๋กœ `app.py` ํŒŒ์ผ์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
370
+ 2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
371
+ 3. ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ํŽ˜์ด์ง€๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋กœ๋“œ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. (404 ์˜ค๋ฅ˜๊ฐ€ ์‚ฌ๋ผ์กŒ๋Š”์ง€ ํ™•์ธ)
372
+ 4. ๋งŒ์•ฝ ํŽ˜์ด์ง€ ๋กœ๋“œ๋Š” ์ •์ƒ์ธ๋ฐ '๋ฌธ์„œ๊ด€๋ฆฌ' ๋˜๋Š” '์žฅ์น˜๊ด€๋ฆฌ' ํƒญ ๋กœ๋”ฉ ๋ฌธ์ œ๊ฐ€ ์—ฌ์ „ํžˆ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด, **๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ ์ฝ˜์†” ๋ฐ ๋„คํŠธ์›Œํฌ ํƒญ ์ •๋ณด**๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•˜์—ฌ ์•Œ๋ ค์ฃผ