jeongsoo commited on
Commit
5542876
ยท
1 Parent(s): b2d2b30
Files changed (1) hide show
  1. app/app.py +155 -209
app/app.py CHANGED
@@ -7,15 +7,11 @@ import json
7
  import logging
8
  import tempfile
9
  import threading
10
- from datetime import datetime, timedelta # datetime ๋Œ€์‹  datetime๊ณผ timedelta ๊ฐœ๋ณ„ ์ž„ํฌํŠธ
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 wrap
15
- import pickle
16
- import os
17
- import gzip
18
- from datetime import datetime
19
 
20
  # ๋กœ๊ฑฐ ์„ค์ •
21
  logging.basicConfig(
@@ -147,133 +143,81 @@ def allowed_doc_file(filename):
147
  # --- ํ—ฌํผ ํ•จ์ˆ˜ ๋ ---
148
 
149
 
150
- def save_embeddings(base_retriever, file_path):
151
- """์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•˜์—ฌ ํŒŒ์ผ์— ์ €์žฅ
152
-
153
- Args:
154
- base_retriever: ์ €์žฅํ•  ๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด
155
- file_path: ์ €์žฅํ•  ํŒŒ์ผ ๊ฒฝ๋กœ
156
-
157
- Returns:
158
- bool: ์ €์žฅ ์„ฑ๊ณต ์—ฌ๋ถ€
159
- """
160
- try:
161
- # ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
162
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
163
-
164
- # ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€
165
- save_data = {
166
- 'timestamp': datetime.now().isoformat(),
167
- 'retriever': base_retriever
168
- }
169
-
170
- # ์••์ถ•ํ•˜์—ฌ ์ €์žฅ (์šฉ๋Ÿ‰ ์ค„์ด๊ธฐ)
171
- with gzip.open(file_path, 'wb') as f:
172
- pickle.dump(save_data, f)
173
-
174
- logger.info(f"์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ {file_path}์— ์••์ถ•ํ•˜์—ฌ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.")
175
- return True
176
- except Exception as e:
177
- logger.error(f"์ž„๋ฒ ๋”ฉ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
178
- return False
179
-
180
- def load_embeddings(file_path, max_age_days=30):
181
- """์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์ผ์—์„œ ๋กœ๋“œ
182
-
183
- Args:
184
- file_path: ๋กœ๋“œํ•  ํŒŒ์ผ ๊ฒฝ๋กœ
185
- max_age_days: ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฒฝ๊ณผ ์ผ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 30์ผ)
186
-
187
- Returns:
188
- object or None: ๋กœ๋“œ๋œ ๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ๋˜๋Š” ์‹คํŒจ ์‹œ None
189
- """
190
- try:
191
- if not os.path.exists(file_path):
192
- logger.info(f"์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ํŒŒ์ผ({file_path})์ด ์—†์Šต๋‹ˆ๋‹ค.")
193
- return None
194
-
195
- # ์••์ถ• ํŒŒ์ผ ๋กœ๋“œ
196
- with gzip.open(file_path, 'rb') as f:
197
- data = pickle.load(f)
198
-
199
- # ํƒ€์ž„์Šคํƒฌํ”„ ํ™•์ธ (๋„ˆ๋ฌด ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
200
- saved_time = datetime.fromisoformat(data['timestamp'])
201
- age = (datetime.now() - saved_time).days
202
-
203
- if age > max_age_days:
204
- logger.info(f"์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ์ด {age}์ผ๋กœ ๋„ˆ๋ฌด ์˜ค๋ž˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.")
205
- return None
206
-
207
- logger.info(f"{file_path}์—์„œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค. (์ƒ์„ฑ์ผ: {saved_time})")
208
- return data['retriever']
209
- except Exception as e:
210
- logger.error(f"์ž„๋ฒ ๋”ฉ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
211
- return None
212
-
213
  def init_retriever():
214
  """๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋กœ๋“œ"""
215
  global base_retriever, retriever
216
 
217
- # ์ž„๋ฒ ๋”ฉ ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ
218
- cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
219
-
220
- # ๋จผ์ € ์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ๋„
221
- cached_retriever = load_embeddings(cache_path)
222
-
223
- if cached_retriever:
224
- logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค.")
225
- base_retriever = cached_retriever
226
- else:
227
- # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹์œผ๋กœ ์ดˆ๊ธฐํ™”
228
- index_path = app.config['INDEX_PATH']
229
-
230
- # VectorRetriever ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™” (์‹ค์ œ ํด๋ž˜์Šค ์‚ฌ์šฉ ๊ฐ€์ •)
231
- if os.path.exists(os.path.join(index_path, "documents.json")): # ๊ฐ„๋‹จํ•œ ์กด์žฌ ํ™•์ธ ์˜ˆ์‹œ
232
  try:
233
- logger.info(f"๊ธฐ์กด ๋ฒกํ„ฐ ์ธ๋ฑ์Šค๋ฅผ '{index_path}'์—์„œ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค...")
234
- base_retriever = VectorRetriever.load(index_path)
235
- logger.info(f"{len(base_retriever.documents) if hasattr(base_retriever, 'documents') else 0}๊ฐœ ๋ฌธ์„œ๊ฐ€ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
236
- except Exception as e:
237
- logger.error(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}. ์ƒˆ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.")
238
  base_retriever = VectorRetriever()
239
- else:
240
- logger.info("๊ธฐ์กด ์ธ๋ฑ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด ์ƒˆ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค...")
 
 
 
 
 
241
  base_retriever = VectorRetriever()
 
 
 
 
242
 
243
- # ๋ฐ์ดํ„ฐ ํด๋”์˜ ๋ฌธ์„œ ๋กœ๋“œ (์˜ˆ์‹œ: base_retriever๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ)
244
- data_path = app.config['DATA_FOLDER']
245
- # base_retriever.documents ์™€ ๊ฐ™์€ ์†์„ฑ์ด ์‹ค์ œ ํด๋ž˜์Šค์— ์žˆ๋‹ค๊ณ  ๊ฐ€์ •
246
- if (not hasattr(base_retriever, 'documents') or not base_retriever.documents) and os.path.exists(data_path):
247
- logger.info(f"{data_path}์—์„œ ๋ฌธ์„œ๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค...")
248
- try:
249
- docs = DocumentProcessor.load_documents_from_directory(
250
- data_path,
251
- extensions=[".txt", ".md", ".csv"], # .pdf, .docx ๋“ฑ์€ ๋ณ„๋„ ์ฒ˜๋ฆฌ ํ•„์š”
252
- recursive=True
253
- )
254
- if docs and hasattr(base_retriever, 'add_documents'):
255
- logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
256
- base_retriever.add_documents(docs)
257
-
258
- if hasattr(base_retriever, 'save'):
259
- logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ '{index_path}'์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
260
- try:
261
- base_retriever.save(index_path)
262
- logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
263
-
264
- # ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ๊ฒ€์ƒ‰๊ธฐ ์บ์‹ฑ
265
- if hasattr(base_retriever, 'documents') and base_retriever.documents:
266
- save_embeddings(base_retriever, cache_path)
267
- logger.info(f"๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์บ์‹œ ํŒŒ์ผ {cache_path}์— ์ €์žฅ ์™„๋ฃŒ")
268
- except Exception as e:
269
- logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
270
- except Exception as e:
271
- logger.error(f"DATA_FOLDER์—์„œ ๋ฌธ์„œ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {e}")
272
 
273
- # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
274
- logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  try:
276
- # ์ž์ฒด ๊ตฌํ˜„๋œ ์žฌ์ˆœ์œ„ํ™” ํ•จ์ˆ˜
 
277
  def custom_rerank_fn(query, results):
278
  query_terms = set(query.lower().split())
279
  for result in results:
@@ -283,94 +227,105 @@ def init_retriever():
283
  normalized_score = term_freq / (len(text.split()) + 1) * 10
284
  result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
285
  elif isinstance(result, dict):
286
- result["rerank_score"] = result.get("score", 0)
287
- # ๊ฒฐ๊ณผ ํ˜•์‹์ด ๋‹ค๋ฅผ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ ํ•„์š”
288
  results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
289
  return results
 
290
 
291
  # ReRanker ํด๋ž˜์Šค ์‚ฌ์šฉ
292
  retriever = ReRanker(
293
  base_retriever=base_retriever,
294
- rerank_fn=custom_rerank_fn, # ๋˜๋Š” ์‹ค์ œ CrossEncoder ๋ชจ๋ธ ์‚ฌ์šฉ
295
- rerank_field="text" # ์žฌ์ˆœ์œ„ํ™”์— ์‚ฌ์šฉํ•  ํ…์ŠคํŠธ ํ•„๋“œ ์ง€์ •
296
  )
297
- logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
298
- except Exception as e:
299
- logger.error(f"์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
300
- retriever = base_retriever # ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์‚ฌ์šฉ
 
301
 
 
302
  return retriever
303
 
304
  def background_init():
305
  """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์ˆ˜ํ–‰"""
306
- global app_ready, retriever, base_retriever
307
-
308
- # ์ฆ‰์‹œ ์•ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ • (์ค‘์š”!)
309
- app_ready = True
310
- logger.info("์•ฑ์„ ์ฆ‰์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ • (app_ready=True)")
311
-
312
  try:
313
- # ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” (๋ณดํ—˜)
314
- if base_retriever is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  base_retriever = MockComponent()
316
- if hasattr(base_retriever, 'documents'):
317
- base_retriever.documents = []
318
-
319
- # ์ž„์‹œ retriever ์„ค์ • (๋น ๋ฅธ ์‹œ์ž‘์„ ์œ„ํ•ด)
320
- if retriever is None:
321
  retriever = MockComponent()
322
- if not hasattr(retriever, 'search'):
323
- retriever.search = lambda query, **kwargs: []
324
-
325
- # ์ž„๋ฒ ๋”ฉ ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ
326
- cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
327
-
328
- # ์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ ๋กœ๋“œ ์‹œ๋„ (๋น ๋ฅธ ์‹œ์ž‘์„ ์œ„ํ•ด)
329
- cached_retriever = load_embeddings(cache_path)
330
-
331
- if cached_retriever:
332
- # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉ
333
- base_retriever = cached_retriever
334
-
335
- # ๊ฐ„๋‹จํ•œ ์žฌ์ˆœ์œ„ํ™” ํ•จ์ˆ˜
336
- def simple_rerank(query, results):
337
- # ๊ฒฐ๊ณผ ์ ์ˆ˜ ์œ ์ง€ํ•˜๋ฉด์„œ ์ •๋ ฌ
338
- if results:
339
- for result in results:
340
- if isinstance(result, dict):
341
- result["rerank_score"] = result.get("score", 0)
342
- results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
343
- return results
344
-
345
- # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
346
- retriever = ReRanker(
347
- base_retriever=base_retriever,
348
- rerank_fn=simple_rerank,
349
- rerank_field="text"
350
- )
351
-
352
- logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ์œผ๋กœ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ (๋น ๋ฅธ ์‹œ์ž‘)")
353
- else:
354
- # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ „์ฒด ์ดˆ๊ธฐํ™” ์ง„ํ–‰
355
- logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ์ด ์—†์–ด ์ „์ฒด ์ดˆ๊ธฐํ™” ์‹œ์ž‘")
356
- # ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๋Š” ์ž‘์—…์ด๋ฏ€๋กœ ๋ณ„๋„ ์Šค๋ ˆ๋“œ๋กœ ์‹คํ–‰ํ•  ์ˆ˜๋„ ์žˆ์Œ
357
- retriever = init_retriever()
358
- logger.info("์ „์ฒด ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
359
-
360
- logger.info("์•ฑ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ (๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ์ค€๋น„๋จ)")
361
  except Exception as e:
362
  logger.error(f"์•ฑ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
363
- # ์ดˆ๊ธฐํ™” ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ๊ฐ์ฒด ์ƒ์„ฑ (์•ฑ ์‹คํ–‰์„ ์œ„ํ•œ ๋ณดํ—˜)
364
- if base_retriever is None:
365
- base_retriever = MockComponent()
366
- if hasattr(base_retriever, 'documents'):
367
- base_retriever.documents = []
368
- if retriever is None:
369
- retriever = MockComponent()
370
- if not hasattr(retriever, 'search'):
371
- retriever.search = lambda query, **kwargs: []
372
-
373
- logger.warning("์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์ง€๋งŒ ์•ฑ์€ ๊ณ„์† ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
374
 
375
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ์‹œ์ž‘ ๋ถ€๋ถ„์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€
376
  init_thread = threading.Thread(target=background_init)
@@ -780,7 +735,7 @@ def voice_chat():
780
  "details": str(e)
781
  }), 500
782
 
783
- # ๋ฌธ์„œ ์—…๋กœ๋“œ API ์—”๋“œํฌ์ธํŠธ์—์„œ ์บ์‹œ ์—…๋ฐ์ดํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€
784
  @app.route('/api/upload', methods=['POST'])
785
  @login_required
786
  def upload_document():
@@ -823,6 +778,7 @@ def upload_document():
823
  logger.error(f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜ ({filename}): {e_read}")
824
  return jsonify({"error": f"ํŒŒ์ผ ์ฝ๊ธฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e_read)}"}), 500
825
 
 
826
  # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ ๋ฌธ์„œ ๋ถ„ํ• /์ฒ˜๋ฆฌ
827
  metadata = {
828
  "source": filename, "filename": filename,
@@ -863,30 +819,20 @@ def upload_document():
863
  logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
864
  base_retriever.add_documents(docs)
865
 
866
- # ์ธ๋ฑ์Šค ์ €์žฅ ๋ฐ ์บ์‹ฑ
867
  logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
868
  index_path = app.config['INDEX_PATH']
869
  try:
870
- # ๊ธฐ์กด ์ธ๋ฑ์Šค ์ €์žฅ
871
  base_retriever.save(index_path)
872
  logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
873
-
874
- # ์ž„๋ฒ ๋”ฉ ์บ์‹œ ์—…๋ฐ์ดํŠธ
875
- cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
876
- if save_embeddings(base_retriever, cache_path):
877
- logger.info("์ž„๋ฒ ๋”ฉ ์บ์‹œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ")
878
-
879
- # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ๋„ ์—…๋ฐ์ดํŠธ ํ•„์š” ์‹œ (๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์œผ๋ฏ€๋กœ)
880
- if hasattr(retriever, 'base_retriever'):
881
- retriever.base_retriever = base_retriever
882
- logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ์˜ ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ")
883
-
884
  return jsonify({
885
  "success": True,
886
  "message": f"ํŒŒ์ผ '{filename}' ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ({len(docs)}๊ฐœ ์ฒญํฌ ์ถ”๊ฐ€)."
887
  })
888
  except Exception as e_save:
889
- logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ๋˜๋Š” ์บ์‹ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e_save}")
890
  return jsonify({"error": f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {str(e_save)}"}), 500
891
  else:
892
  logger.warning(f"ํŒŒ์ผ '{filename}'์—์„œ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค.")
 
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(
 
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:
 
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)
 
735
  "details": str(e)
736
  }), 500
737
 
738
+
739
  @app.route('/api/upload', methods=['POST'])
740
  @login_required
741
  def upload_document():
 
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,
 
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}'์—์„œ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค.")