ginipick commited on
Commit
9723839
Β·
verified Β·
1 Parent(s): 147ca0f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +724 -12
app.py CHANGED
@@ -7,6 +7,8 @@ import asyncio
7
  import logging
8
  import threading
9
  import concurrent.futures
 
 
10
 
11
  # λ‘œκΉ… μ„€μ •
12
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
@@ -37,15 +39,26 @@ if not METADATA_DIR.exists():
37
  METADATA_DIR.mkdir(parents=True)
38
  PDF_METADATA_FILE = METADATA_DIR / "pdf_metadata.json"
39
 
 
 
 
 
 
40
  # κ΄€λ¦¬μž λΉ„λ°€λ²ˆν˜Έ
41
  ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # ν™˜κ²½ λ³€μˆ˜μ—μ„œ κ°€μ Έμ˜€κΈ°, 기본값은 ν…ŒμŠ€νŠΈμš©
42
 
 
 
 
 
43
  # μ „μ—­ μΊμ‹œ 객체
44
  pdf_cache: Dict[str, Dict[str, Any]] = {}
45
  # 캐싱 락
46
  cache_locks = {}
47
  # PDF 메타데이터 (ID to 경둜 λ§€ν•‘)
48
  pdf_metadata: Dict[str, str] = {}
 
 
49
 
50
  # PDF 메타데이터 λ‘œλ“œ
51
  def load_pdf_metadata():
@@ -69,7 +82,6 @@ def save_pdf_metadata():
69
  except Exception as e:
70
  logger.error(f"메타데이터 μ €μž₯ 였λ₯˜: {e}")
71
 
72
- # PDF ID 생성 (파일λͺ… + νƒ€μž„μŠ€νƒ¬ν”„ 기반)
73
  # PDF ID 생성 (파일λͺ… + νƒ€μž„μŠ€νƒ¬ν”„ 기반) - 더 λ‹¨μˆœν•˜κ³  μ•ˆμ „ν•œ λ°©μ‹μœΌλ‘œ λ³€κ²½
74
  def generate_pdf_id(filename: str) -> str:
75
  # 파일λͺ…μ—μ„œ ν™•μž₯자 제거
@@ -83,8 +95,6 @@ def generate_pdf_id(filename: str) -> str:
83
  random_suffix = uuid.uuid4().hex[:6]
84
  return f"{safe_name}_{timestamp}_{random_suffix}"
85
 
86
-
87
-
88
  # PDF 파일 λͺ©λ‘ κ°€μ Έμ˜€κΈ° (메인 λ””λ ‰ν† λ¦¬μš©)
89
  def get_pdf_files():
90
  pdf_files = []
@@ -146,6 +156,164 @@ def generate_pdf_projects():
146
  def get_cache_path(pdf_name: str):
147
  return CACHE_DIR / f"{pdf_name}_cache.json"
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  # μ΅œμ ν™”λœ PDF νŽ˜μ΄μ§€ 캐싱 ν•¨μˆ˜
150
  async def cache_pdf(pdf_path: str):
151
  try:
@@ -300,7 +468,6 @@ async def cache_pdf(pdf_path: str):
300
  pdf_cache[pdf_name]["status"] = "error"
301
  pdf_cache[pdf_name]["error"] = str(e)
302
 
303
- # PDF ID둜 PDF 경둜 찾기
304
  # PDF ID둜 PDF 경둜 μ°ΎκΈ° (κ°œμ„ λœ 검색 둜직)
305
  def get_pdf_path_by_id(pdf_id: str) -> str:
306
  logger.info(f"PDF ID둜 파일 쑰회: {pdf_id}")
@@ -534,6 +701,50 @@ async def get_cache_status(path: str = None):
534
  return {name: {"status": info["status"], "progress": info.get("progress", 0)}
535
  for name, info in pdf_cache.items()}
536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  # API μ—”λ“œν¬μΈνŠΈ: μΊμ‹œλœ PDF μ½˜ν…μΈ  제곡 (점진적 λ‘œλ”© 지원)
538
  @app.get("/api/cached-pdf")
539
  async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
@@ -818,7 +1029,7 @@ async def root(request: Request, pdf_id: Optional[str] = Query(None)):
818
  return RedirectResponse(url=f"/view/{pdf_id}")
819
  return get_html_content()
820
 
821
- # HTML λ¬Έμžμ—΄ (UI μˆ˜μ • 버전)
822
  HTML = """
823
  <!doctype html>
824
  <html lang="ko">
@@ -844,6 +1055,8 @@ HTML = """
844
  --secondary-color: #ffd6e0; /* νŒŒμŠ€ν…” 핑크 */
845
  --tertiary-color: #c3fae8; /* νŒŒμŠ€ν…” 민트 */
846
  --accent-color: #d0bfff; /* νŒŒμŠ€ν…” νΌν”Œ */
 
 
847
  --bg-color: #f8f9fa; /* 밝은 λ°°κ²½ */
848
  --text-color: #495057; /* λΆ€λ“œλŸ¬μš΄ μ–΄λ‘μš΄ 색 */
849
  --card-bg: #ffffff; /* μΉ΄λ“œ 배경색 */
@@ -873,7 +1086,7 @@ HTML = """
873
  }
874
 
875
  /* 헀더 제λͺ© 제거 및 Home λ²„νŠΌ λ ˆμ΄μ–΄ 처리 */
876
- .floating-home {
877
  position: fixed;
878
  top: 20px;
879
  left: 20px;
@@ -892,12 +1105,17 @@ HTML = """
892
  overflow: hidden;
893
  }
894
 
895
- .floating-home:hover {
 
 
 
 
 
896
  transform: scale(1.05);
897
  box-shadow: var(--shadow-lg);
898
  }
899
 
900
- .floating-home .icon {
901
  display: flex;
902
  justify-content: center;
903
  align-items: center;
@@ -908,11 +1126,19 @@ HTML = """
908
  transition: var(--transition);
909
  }
910
 
 
 
 
 
911
  .floating-home:hover .icon {
912
  color: #8bc5f8;
913
  }
914
 
915
- .floating-home .title {
 
 
 
 
916
  position: absolute;
917
  left: 70px;
918
  background: rgba(255, 255, 255, 0.95);
@@ -928,7 +1154,7 @@ HTML = """
928
  transition: all 0.3s ease;
929
  }
930
 
931
- .floating-home:hover .title {
932
  opacity: 1;
933
  transform: translateX(0);
934
  }
@@ -1472,6 +1698,257 @@ HTML = """
1472
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
1473
  }
1474
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1475
  /* λ°˜μ‘ν˜• λ””μžμΈ */
1476
  @media (max-width: 768px) {
1477
  .grid {
@@ -1488,12 +1965,12 @@ HTML = """
1488
  padding: 10px 20px;
1489
  }
1490
 
1491
- .floating-home {
1492
  width: 50px;
1493
  height: 50px;
1494
  }
1495
 
1496
- .floating-home .icon {
1497
  font-size: 18px;
1498
  }
1499
 
@@ -1501,6 +1978,10 @@ HTML = """
1501
  padding: 6px 15px;
1502
  font-size: 12px;
1503
  }
 
 
 
 
1504
  }
1505
  </style>
1506
  </head>
@@ -1511,6 +1992,25 @@ HTML = """
1511
  <div class="title">ν™ˆμœΌλ‘œ λŒμ•„κ°€κΈ°</div>
1512
  </div>
1513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1514
  <!-- κ΄€λ¦¬μž λ²„νŠΌ -->
1515
  <div id="adminButton">
1516
  <i class="fas fa-cog"></i> Admin
@@ -1588,6 +2088,11 @@ HTML = """
1588
  let audioInitialized = false;
1589
  let audioContext = null;
1590
 
 
 
 
 
 
1591
  // μ˜€λ””μ˜€ μ΄ˆκΈ°ν™” ν•¨μˆ˜
1592
  function initializeAudio() {
1593
  if (audioInitialized) return Promise.resolve();
@@ -1685,6 +2190,158 @@ HTML = """
1685
  /* ── μœ ν‹Έ ── */
1686
  function $id(id){return document.getElementById(id)}
1687
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1688
  // DOM이 λ‘œλ“œλ˜λ©΄ μ‹€ν–‰
1689
  document.addEventListener('DOMContentLoaded', function() {
1690
  console.log("DOM λ‘œλ“œ μ™„λ£Œ, 이벀트 μ„€μ • μ‹œμž‘");
@@ -1746,6 +2403,40 @@ HTML = """
1746
  $id('loadingPages').style.display = 'none';
1747
  currentLoadingPdfPath = null;
1748
  currentPdfId = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1749
  });
1750
  }
1751
  });
@@ -2015,6 +2706,8 @@ HTML = """
2015
  createFlipBook(cachedData.pages);
2016
  // ν˜„μž¬ μ—΄λ¦° PDF의 ID μ €μž₯
2017
  currentPdfId = pdfId;
 
 
2018
  return;
2019
  }
2020
  } catch (error) {
@@ -2054,6 +2747,8 @@ HTML = """
2054
 
2055
  // ν˜„μž¬ μ—΄λ¦° PDF의 ID μ €μž₯
2056
  currentPdfId = pdfId;
 
 
2057
  }
2058
  } catch (error) {
2059
  console.error("PDF ID둜 μ—΄κΈ° μ‹€νŒ¨:", error);
@@ -2092,10 +2787,19 @@ HTML = """
2092
  const card = document.querySelectorAll('.card')[i];
2093
  if (card && card.dataset.pdfId) {
2094
  currentPdfId = card.dataset.pdfId;
 
 
2095
  } else {
2096
  currentPdfId = null;
 
 
2097
  }
2098
 
 
 
 
 
 
2099
  // κΈ°μ‘΄ FlipBook 정리
2100
  if(fb) {
2101
  fb.destroy();
@@ -2430,6 +3134,14 @@ HTML = """
2430
  $id('homeButton').style.display=showHome?'none':'block';
2431
  $id('adminPage').style.display='none';
2432
 
 
 
 
 
 
 
 
 
2433
  // λ·°μ–΄ λͺ¨λ“œμΌ λ•Œ μŠ€νƒ€μΌ λ³€κ²½
2434
  if(!showHome) {
2435
  document.body.classList.add('viewer-mode');
 
7
  import logging
8
  import threading
9
  import concurrent.futures
10
+ from openai import OpenAI
11
+ import fitz # PyMuPDF
12
 
13
  # λ‘œκΉ… μ„€μ •
14
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
39
  METADATA_DIR.mkdir(parents=True)
40
  PDF_METADATA_FILE = METADATA_DIR / "pdf_metadata.json"
41
 
42
+ # μž„λ² λ”© μΊμ‹œ 디렉토리 μ„€μ •
43
+ EMBEDDING_DIR = pathlib.Path("/data/embeddings") if os.path.exists("/data") else BASE / "embeddings"
44
+ if not EMBEDDING_DIR.exists():
45
+ EMBEDDING_DIR.mkdir(parents=True)
46
+
47
  # κ΄€λ¦¬μž λΉ„λ°€λ²ˆν˜Έ
48
  ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # ν™˜κ²½ λ³€μˆ˜μ—μ„œ κ°€μ Έμ˜€κΈ°, 기본값은 ν…ŒμŠ€νŠΈμš©
49
 
50
+ # OpenAI API ν‚€ μ„€μ •
51
+ OPENAI_API_KEY = os.getenv("LLM_API", "")
52
+ openai_client = OpenAI(api_key=OPENAI_API_KEY)
53
+
54
  # μ „μ—­ μΊμ‹œ 객체
55
  pdf_cache: Dict[str, Dict[str, Any]] = {}
56
  # 캐싱 락
57
  cache_locks = {}
58
  # PDF 메타데이터 (ID to 경둜 λ§€ν•‘)
59
  pdf_metadata: Dict[str, str] = {}
60
+ # PDF μž„λ² λ”© μΊμ‹œ
61
+ pdf_embeddings: Dict[str, Dict[str, Any]] = {}
62
 
63
  # PDF 메타데이터 λ‘œλ“œ
64
  def load_pdf_metadata():
 
82
  except Exception as e:
83
  logger.error(f"메타데이터 μ €μž₯ 였λ₯˜: {e}")
84
 
 
85
  # PDF ID 생성 (파일λͺ… + νƒ€μž„μŠ€νƒ¬ν”„ 기반) - 더 λ‹¨μˆœν•˜κ³  μ•ˆμ „ν•œ λ°©μ‹μœΌλ‘œ λ³€κ²½
86
  def generate_pdf_id(filename: str) -> str:
87
  # 파일λͺ…μ—μ„œ ν™•μž₯자 제거
 
95
  random_suffix = uuid.uuid4().hex[:6]
96
  return f"{safe_name}_{timestamp}_{random_suffix}"
97
 
 
 
98
  # PDF 파일 λͺ©λ‘ κ°€μ Έμ˜€κΈ° (메인 λ””λ ‰ν† λ¦¬μš©)
99
  def get_pdf_files():
100
  pdf_files = []
 
156
  def get_cache_path(pdf_name: str):
157
  return CACHE_DIR / f"{pdf_name}_cache.json"
158
 
159
+ # μž„λ² λ”© μΊμ‹œ 파일 경둜 생성
160
+ def get_embedding_path(pdf_id: str):
161
+ return EMBEDDING_DIR / f"{pdf_id}_embedding.json"
162
+
163
+ # PDF ν…μŠ€νŠΈ μΆ”μΆœ ν•¨μˆ˜
164
+ def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]:
165
+ try:
166
+ doc = fitz.open(pdf_path)
167
+ chunks = []
168
+
169
+ for page_num in range(len(doc)):
170
+ page = doc[page_num]
171
+ text = page.get_text()
172
+
173
+ # νŽ˜μ΄μ§€ ν…μŠ€νŠΈκ°€ μžˆλŠ” 경우만 μΆ”κ°€
174
+ if text.strip():
175
+ chunks.append({
176
+ "page": page_num + 1,
177
+ "text": text,
178
+ "chunk_id": f"page_{page_num + 1}"
179
+ })
180
+
181
+ return chunks
182
+ except Exception as e:
183
+ logger.error(f"PDF ν…μŠ€νŠΈ μΆ”μΆœ 였λ₯˜: {e}")
184
+ return []
185
+
186
+ # PDF ID둜 μž„λ² λ”© 생성 λ˜λŠ” κ°€μ Έμ˜€κΈ°
187
+ async def get_pdf_embedding(pdf_id: str) -> Dict[str, Any]:
188
+ try:
189
+ # μž„λ² λ”© μΊμ‹œ 확인
190
+ embedding_path = get_embedding_path(pdf_id)
191
+ if embedding_path.exists():
192
+ try:
193
+ with open(embedding_path, "r", encoding="utf-8") as f:
194
+ return json.load(f)
195
+ except Exception as e:
196
+ logger.error(f"μž„λ² λ”© μΊμ‹œ λ‘œλ“œ 였λ₯˜: {e}")
197
+
198
+ # PDF 경둜 찾기
199
+ pdf_path = get_pdf_path_by_id(pdf_id)
200
+ if not pdf_path:
201
+ raise ValueError(f"PDF ID {pdf_id}에 ν•΄λ‹Ήν•˜λŠ” νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€")
202
+
203
+ # ν…μŠ€νŠΈ μΆ”μΆœ
204
+ chunks = extract_pdf_text(pdf_path)
205
+ if not chunks:
206
+ raise ValueError(f"PDFμ—μ„œ ν…μŠ€νŠΈλ₯Ό μΆ”μΆœν•  수 μ—†μŠ΅λ‹ˆλ‹€: {pdf_path}")
207
+
208
+ # μž„λ² λ”© μ €μž₯ 및 λ°˜ν™˜
209
+ embedding_data = {
210
+ "pdf_id": pdf_id,
211
+ "pdf_path": pdf_path,
212
+ "chunks": chunks,
213
+ "created_at": time.time()
214
+ }
215
+
216
+ # μž„λ² λ”© μΊμ‹œ μ €μž₯
217
+ with open(embedding_path, "w", encoding="utf-8") as f:
218
+ json.dump(embedding_data, f, ensure_ascii=False)
219
+
220
+ return embedding_data
221
+
222
+ except Exception as e:
223
+ logger.error(f"PDF μž„λ² λ”© 생성 였λ₯˜: {e}")
224
+ return {"error": str(e), "pdf_id": pdf_id}
225
+
226
+ # PDF λ‚΄μš© 기반 μ§ˆμ˜μ‘λ‹΅
227
+ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
228
+ try:
229
+ # μž„λ² λ”© 데이터 κ°€μ Έμ˜€κΈ°
230
+ embedding_data = await get_pdf_embedding(pdf_id)
231
+ if "error" in embedding_data:
232
+ return {"error": embedding_data["error"]}
233
+
234
+ # 청크 ν…μŠ€νŠΈ λͺ¨μœΌκΈ° (μž„μ‹œλ‘œ κ°„λ‹¨ν•˜κ²Œ 전체 ν…μŠ€νŠΈ μ‚¬μš©)
235
+ all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
236
+
237
+ # OpenAI API 호좜
238
+ # μ»¨ν…μŠ€νŠΈ 크기λ₯Ό κ³ λ €ν•˜μ—¬ ν…μŠ€νŠΈκ°€ λ„ˆλ¬΄ κΈΈλ©΄ μ•žλΆ€λΆ„λ§Œ μ‚¬μš©
239
+ max_context_length = 60000 # 토큰 μˆ˜κ°€ μ•„λ‹Œ 문자 수 κΈ°μ€€ (λŒ€λž΅μ μΈ μ œν•œ)
240
+ if len(all_text) > max_context_length:
241
+ all_text = all_text[:max_context_length] + "...(μ΄ν•˜ μƒλž΅)"
242
+
243
+ # μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ μ€€λΉ„
244
+ system_prompt = """
245
+ 당신은 PDF λ‚΄μš©μ„ 기반으둜 μ§ˆλ¬Έμ— λ‹΅λ³€ν•˜λŠ” λ„μš°λ―Έμž…λ‹ˆλ‹€. 제곡된 PDF μ»¨ν…μŠ€νŠΈ μ •λ³΄λ§Œμ„ μ‚¬μš©ν•˜μ—¬ λ‹΅λ³€ν•˜μ„Έμš”.
246
+ μ»¨ν…μŠ€νŠΈμ— κ΄€λ ¨ 정보가 μ—†λŠ” 경우, '제곡된 PDFμ—μ„œ ν•΄λ‹Ή 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€'라고 μ†”μ§νžˆ λ‹΅ν•˜μ„Έμš”.
247
+ 닡변은 λͺ…ν™•ν•˜κ³  κ°„κ²°ν•˜κ²Œ μž‘μ„±ν•˜κ³ , κ΄€λ ¨ νŽ˜μ΄μ§€ 번호λ₯Ό μΈμš©ν•˜μ„Έμš”.
248
+ """
249
+
250
+ # gpt-4.1-mini λͺ¨λΈ μ‚¬μš©
251
+ try:
252
+ response = openai_client.chat.completions.create(
253
+ model="gpt-4.1-mini",
254
+ messages=[
255
+ {"role": "system", "content": system_prompt},
256
+ {"role": "user", "content": f"λ‹€μŒ PDF λ‚΄μš©μ„ μ°Έκ³ ν•˜μ—¬ μ§ˆλ¬Έμ— λ‹΅λ³€ν•΄μ£Όμ„Έμš”.\n\nPDF λ‚΄μš©:\n{all_text}\n\n질문: {query}"}
257
+ ],
258
+ temperature=0.7,
259
+ max_tokens=2048
260
+ )
261
+
262
+ answer = response.choices[0].message.content
263
+ return {
264
+ "answer": answer,
265
+ "pdf_id": pdf_id,
266
+ "query": query
267
+ }
268
+ except Exception as api_error:
269
+ logger.error(f"OpenAI API 호좜 였λ₯˜: {api_error}")
270
+ return {"error": f"AI 응닡 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(api_error)}"}
271
+
272
+ except Exception as e:
273
+ logger.error(f"μ§ˆμ˜μ‘λ‹΅ 처리 였λ₯˜: {e}")
274
+ return {"error": str(e)}
275
+
276
+ # PDF μš”μ•½ 생성
277
+ async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
278
+ try:
279
+ # μž„λ² λ”© 데이터 κ°€μ Έμ˜€κΈ°
280
+ embedding_data = await get_pdf_embedding(pdf_id)
281
+ if "error" in embedding_data:
282
+ return {"error": embedding_data["error"]}
283
+
284
+ # 청크 ν…μŠ€νŠΈ λͺ¨μœΌκΈ° (μ œν•œλœ 길이)
285
+ all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
286
+
287
+ # μ»¨ν…μŠ€νŠΈ 크기λ₯Ό κ³ λ €ν•˜μ—¬ ν…μŠ€νŠΈκ°€ λ„ˆλ¬΄ κΈΈλ©΄ μ•žλΆ€λΆ„λ§Œ μ‚¬μš©
288
+ max_context_length = 60000 # 토큰 μˆ˜κ°€ μ•„λ‹Œ 문자 수 κΈ°μ€€ (λŒ€λž΅μ μΈ μ œν•œ)
289
+ if len(all_text) > max_context_length:
290
+ all_text = all_text[:max_context_length] + "...(μ΄ν•˜ μƒλž΅)"
291
+
292
+ # OpenAI API 호좜
293
+ try:
294
+ response = openai_client.chat.completions.create(
295
+ model="gpt-4.1-mini",
296
+ messages=[
297
+ {"role": "system", "content": "λ‹€μŒ PDF λ‚΄μš©μ„ κ°„κ²°ν•˜κ²Œ μš”μ•½ν•΄μ£Όμ„Έμš”. 핡심 μ£Όμ œμ™€ μ£Όμš” 포인트λ₯Ό ν¬ν•¨ν•œ μš”μ•½μ„ 500자 μ΄λ‚΄λ‘œ μž‘μ„±ν•΄μ£Όμ„Έμš”."},
298
+ {"role": "user", "content": f"PDF λ‚΄μš©:\n{all_text}"}
299
+ ],
300
+ temperature=0.7,
301
+ max_tokens=1024
302
+ )
303
+
304
+ summary = response.choices[0].message.content
305
+ return {
306
+ "summary": summary,
307
+ "pdf_id": pdf_id
308
+ }
309
+ except Exception as api_error:
310
+ logger.error(f"OpenAI API 호좜 였λ₯˜: {api_error}")
311
+ return {"error": f"AI μš”μ•½ 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(api_error)}"}
312
+
313
+ except Exception as e:
314
+ logger.error(f"PDF μš”μ•½ 생성 였λ₯˜: {e}")
315
+ return {"error": str(e)}
316
+
317
  # μ΅œμ ν™”λœ PDF νŽ˜μ΄μ§€ 캐싱 ν•¨μˆ˜
318
  async def cache_pdf(pdf_path: str):
319
  try:
 
468
  pdf_cache[pdf_name]["status"] = "error"
469
  pdf_cache[pdf_name]["error"] = str(e)
470
 
 
471
  # PDF ID둜 PDF 경둜 μ°ΎκΈ° (κ°œμ„ λœ 검색 둜직)
472
  def get_pdf_path_by_id(pdf_id: str) -> str:
473
  logger.info(f"PDF ID둜 파일 쑰회: {pdf_id}")
 
701
  return {name: {"status": info["status"], "progress": info.get("progress", 0)}
702
  for name, info in pdf_cache.items()}
703
 
704
+ # API μ—”λ“œν¬μΈνŠΈ: PDF에 λŒ€ν•œ μ§ˆμ˜μ‘λ‹΅
705
+ @app.post("/api/ai/query-pdf/{pdf_id}")
706
+ async def api_query_pdf(pdf_id: str, query: Dict[str, str]):
707
+ try:
708
+ user_query = query.get("query", "")
709
+ if not user_query:
710
+ return JSONResponse(content={"error": "질문이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€"}, status_code=400)
711
+
712
+ # PDF 경둜 확인
713
+ pdf_path = get_pdf_path_by_id(pdf_id)
714
+ if not pdf_path:
715
+ return JSONResponse(content={"error": f"PDF ID {pdf_id}에 ν•΄λ‹Ήν•˜λŠ” νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}, status_code=404)
716
+
717
+ # μ§ˆμ˜μ‘λ‹΅ 처리
718
+ result = await query_pdf(pdf_id, user_query)
719
+
720
+ if "error" in result:
721
+ return JSONResponse(content={"error": result["error"]}, status_code=500)
722
+
723
+ return result
724
+ except Exception as e:
725
+ logger.error(f"μ§ˆμ˜μ‘λ‹΅ API 였λ₯˜: {e}")
726
+ return JSONResponse(content={"error": str(e)}, status_code=500)
727
+
728
+ # API μ—”λ“œν¬μΈνŠΈ: PDF μš”μ•½
729
+ @app.get("/api/ai/summarize-pdf/{pdf_id}")
730
+ async def api_summarize_pdf(pdf_id: str):
731
+ try:
732
+ # PDF 경둜 확인
733
+ pdf_path = get_pdf_path_by_id(pdf_id)
734
+ if not pdf_path:
735
+ return JSONResponse(content={"error": f"PDF ID {pdf_id}에 ν•΄λ‹Ήν•˜λŠ” νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}, status_code=404)
736
+
737
+ # μš”μ•½ 처리
738
+ result = await summarize_pdf(pdf_id)
739
+
740
+ if "error" in result:
741
+ return JSONResponse(content={"error": result["error"]}, status_code=500)
742
+
743
+ return result
744
+ except Exception as e:
745
+ logger.error(f"PDF μš”μ•½ API 였λ₯˜: {e}")
746
+ return JSONResponse(content={"error": str(e)}, status_code=500)
747
+
748
  # API μ—”λ“œν¬μΈνŠΈ: μΊμ‹œλœ PDF μ½˜ν…μΈ  제곡 (점진적 λ‘œλ”© 지원)
749
  @app.get("/api/cached-pdf")
750
  async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
 
1029
  return RedirectResponse(url=f"/view/{pdf_id}")
1030
  return get_html_content()
1031
 
1032
+ # HTML λ¬Έμžμ—΄ (AI λ²„νŠΌ 및 챗봇 UI μΆ”κ°€)
1033
  HTML = """
1034
  <!doctype html>
1035
  <html lang="ko">
 
1055
  --secondary-color: #ffd6e0; /* νŒŒμŠ€ν…” 핑크 */
1056
  --tertiary-color: #c3fae8; /* νŒŒμŠ€ν…” 민트 */
1057
  --accent-color: #d0bfff; /* νŒŒμŠ€ν…” νΌν”Œ */
1058
+ --ai-color: #86e8ab; /* AI λ²„νŠΌ 색상 */
1059
+ --ai-hover: #65d68a; /* AI ν˜Έλ²„ 색상 */
1060
  --bg-color: #f8f9fa; /* 밝은 λ°°κ²½ */
1061
  --text-color: #495057; /* λΆ€λ“œλŸ¬μš΄ μ–΄λ‘μš΄ 색 */
1062
  --card-bg: #ffffff; /* μΉ΄λ“œ 배경색 */
 
1086
  }
1087
 
1088
  /* 헀더 제λͺ© 제거 및 Home λ²„νŠΌ λ ˆμ΄μ–΄ 처리 */
1089
+ .floating-home, .floating-ai {
1090
  position: fixed;
1091
  top: 20px;
1092
  left: 20px;
 
1105
  overflow: hidden;
1106
  }
1107
 
1108
+ .floating-ai {
1109
+ top: 90px; /* Home λ²„νŠΌ μ•„λž˜μ— μœ„μΉ˜ */
1110
+ background: rgba(134, 232, 171, 0.9); /* AI λ²„νŠΌ 색상 */
1111
+ }
1112
+
1113
+ .floating-home:hover, .floating-ai:hover {
1114
  transform: scale(1.05);
1115
  box-shadow: var(--shadow-lg);
1116
  }
1117
 
1118
+ .floating-home .icon, .floating-ai .icon {
1119
  display: flex;
1120
  justify-content: center;
1121
  align-items: center;
 
1126
  transition: var(--transition);
1127
  }
1128
 
1129
+ .floating-ai .icon {
1130
+ color: white;
1131
+ }
1132
+
1133
  .floating-home:hover .icon {
1134
  color: #8bc5f8;
1135
  }
1136
 
1137
+ .floating-ai:hover .icon {
1138
+ color: #ffffff;
1139
+ }
1140
+
1141
+ .floating-home .title, .floating-ai .title {
1142
  position: absolute;
1143
  left: 70px;
1144
  background: rgba(255, 255, 255, 0.95);
 
1154
  transition: all 0.3s ease;
1155
  }
1156
 
1157
+ .floating-home:hover .title, .floating-ai:hover .title {
1158
  opacity: 1;
1159
  transform: translateX(0);
1160
  }
 
1698
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
1699
  }
1700
 
1701
+ /* AI 챗봇 UI μŠ€νƒ€μΌ */
1702
+ #aiChatContainer {
1703
+ display: none;
1704
+ position: fixed;
1705
+ top: 0;
1706
+ right: 0;
1707
+ width: 400px;
1708
+ height: 100%;
1709
+ background: rgba(255, 255, 255, 0.95);
1710
+ backdrop-filter: blur(10px);
1711
+ box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1);
1712
+ z-index: 9999;
1713
+ transition: all 0.3s ease;
1714
+ transform: translateX(100%);
1715
+ padding: 20px;
1716
+ box-sizing: border-box;
1717
+ display: flex;
1718
+ flex-direction: column;
1719
+ }
1720
+
1721
+ #aiChatContainer.active {
1722
+ transform: translateX(0);
1723
+ }
1724
+
1725
+ #aiChatHeader {
1726
+ display: flex;
1727
+ justify-content: space-between;
1728
+ align-items: center;
1729
+ margin-bottom: 15px;
1730
+ padding-bottom: 15px;
1731
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
1732
+ }
1733
+
1734
+ #aiChatHeader h3 {
1735
+ margin: 0;
1736
+ color: #333;
1737
+ font-size: 18px;
1738
+ display: flex;
1739
+ align-items: center;
1740
+ }
1741
+
1742
+ #aiChatHeader h3 i {
1743
+ margin-right: 10px;
1744
+ color: var(--ai-color);
1745
+ }
1746
+
1747
+ #aiChatClose {
1748
+ background: none;
1749
+ border: none;
1750
+ cursor: pointer;
1751
+ font-size: 18px;
1752
+ color: #666;
1753
+ transition: var(--transition);
1754
+ }
1755
+
1756
+ #aiChatClose:hover {
1757
+ color: #333;
1758
+ transform: scale(1.1);
1759
+ }
1760
+
1761
+ #aiChatMessages {
1762
+ flex: 1;
1763
+ overflow-y: auto;
1764
+ padding: 10px 0;
1765
+ margin-bottom: 15px;
1766
+ }
1767
+
1768
+ .chat-message {
1769
+ margin-bottom: 15px;
1770
+ display: flex;
1771
+ align-items: flex-start;
1772
+ }
1773
+
1774
+ .chat-message.user {
1775
+ flex-direction: row-reverse;
1776
+ }
1777
+
1778
+ .chat-avatar {
1779
+ width: 35px;
1780
+ height: 35px;
1781
+ border-radius: 50%;
1782
+ display: flex;
1783
+ justify-content: center;
1784
+ align-items: center;
1785
+ margin-right: 10px;
1786
+ flex-shrink: 0;
1787
+ }
1788
+
1789
+ .chat-message.user .chat-avatar {
1790
+ margin-right: 0;
1791
+ margin-left: 10px;
1792
+ background: var(--primary-color);
1793
+ color: white;
1794
+ }
1795
+
1796
+ .chat-message.ai .chat-avatar {
1797
+ background: var(--ai-color);
1798
+ color: white;
1799
+ }
1800
+
1801
+ .chat-content {
1802
+ background: #f1f1f1;
1803
+ padding: 12px 15px;
1804
+ border-radius: 18px;
1805
+ max-width: 75%;
1806
+ word-break: break-word;
1807
+ position: relative;
1808
+ font-size: 14px;
1809
+ line-height: 1.4;
1810
+ }
1811
+
1812
+ .chat-message.user .chat-content {
1813
+ background: var(--primary-color);
1814
+ color: white;
1815
+ border-bottom-right-radius: 4px;
1816
+ }
1817
+
1818
+ .chat-message.ai .chat-content {
1819
+ background: #f1f1f1;
1820
+ color: #333;
1821
+ border-bottom-left-radius: 4px;
1822
+ }
1823
+
1824
+ #aiChatForm {
1825
+ display: flex;
1826
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
1827
+ padding-top: 15px;
1828
+ }
1829
+
1830
+ #aiChatInput {
1831
+ flex: 1;
1832
+ padding: 12px 15px;
1833
+ border: 1px solid #ddd;
1834
+ border-radius: 25px;
1835
+ font-size: 14px;
1836
+ outline: none;
1837
+ transition: var(--transition);
1838
+ }
1839
+
1840
+ #aiChatInput:focus {
1841
+ border-color: var(--ai-color);
1842
+ box-shadow: 0 0 0 2px rgba(134, 232, 171, 0.2);
1843
+ }
1844
+
1845
+ #aiChatSubmit {
1846
+ background: var(--ai-color);
1847
+ border: none;
1848
+ color: white;
1849
+ width: 45px;
1850
+ height: 45px;
1851
+ border-radius: 50%;
1852
+ margin-left: 10px;
1853
+ display: flex;
1854
+ justify-content: center;
1855
+ align-items: center;
1856
+ cursor: pointer;
1857
+ transition: var(--transition);
1858
+ }
1859
+
1860
+ #aiChatSubmit:hover {
1861
+ background: var(--ai-hover);
1862
+ transform: scale(1.05);
1863
+ }
1864
+
1865
+ #aiChatSubmit:disabled {
1866
+ background: #ccc;
1867
+ cursor: not-allowed;
1868
+ }
1869
+
1870
+ .typing-indicator {
1871
+ display: flex;
1872
+ align-items: center;
1873
+ margin-top: 5px;
1874
+ font-size: 12px;
1875
+ color: #666;
1876
+ }
1877
+
1878
+ .typing-indicator span {
1879
+ height: 8px;
1880
+ width: 8px;
1881
+ background: var(--ai-color);
1882
+ border-radius: 50%;
1883
+ display: inline-block;
1884
+ margin-right: 3px;
1885
+ animation: typing 1s infinite;
1886
+ }
1887
+
1888
+ .typing-indicator span:nth-child(2) {
1889
+ animation-delay: 0.2s;
1890
+ }
1891
+
1892
+ .typing-indicator span:nth-child(3) {
1893
+ animation-delay: 0.4s;
1894
+ }
1895
+
1896
+ @keyframes typing {
1897
+ 0% { transform: translateY(0); }
1898
+ 50% { transform: translateY(-5px); }
1899
+ 100% { transform: translateY(0); }
1900
+ }
1901
+
1902
+ .chat-time {
1903
+ font-size: 10px;
1904
+ color: #999;
1905
+ margin-top: 5px;
1906
+ text-align: right;
1907
+ }
1908
+
1909
+ /* μ½”λ“œ 블둝 μŠ€νƒ€μΌ */
1910
+ .chat-content pre {
1911
+ background: rgba(0, 0, 0, 0.05);
1912
+ padding: 10px;
1913
+ border-radius: 5px;
1914
+ overflow-x: auto;
1915
+ font-family: monospace;
1916
+ font-size: 12px;
1917
+ margin: 10px 0;
1918
+ }
1919
+
1920
+ /* λ§ˆν¬λ‹€μš΄ μŠ€νƒ€μΌ */
1921
+ .chat-content strong {
1922
+ font-weight: bold;
1923
+ }
1924
+
1925
+ .chat-content em {
1926
+ font-style: italic;
1927
+ }
1928
+
1929
+ .chat-content ul, .chat-content ol {
1930
+ margin-left: 20px;
1931
+ margin-top: 5px;
1932
+ margin-bottom: 5px;
1933
+ }
1934
+
1935
+ /* 곡유 λ²„νŠΌ */
1936
+ #shareChat {
1937
+ padding: 8px 15px;
1938
+ background: #f1f1f1;
1939
+ border: none;
1940
+ border-radius: 20px;
1941
+ font-size: 12px;
1942
+ color: #666;
1943
+ cursor: pointer;
1944
+ margin-top: 5px;
1945
+ transition: var(--transition);
1946
+ }
1947
+
1948
+ #shareChat:hover {
1949
+ background: #ddd;
1950
+ }
1951
+
1952
  /* λ°˜μ‘ν˜• λ””μžμΈ */
1953
  @media (max-width: 768px) {
1954
  .grid {
 
1965
  padding: 10px 20px;
1966
  }
1967
 
1968
+ .floating-home, .floating-ai {
1969
  width: 50px;
1970
  height: 50px;
1971
  }
1972
 
1973
+ .floating-home .icon, .floating-ai .icon {
1974
  font-size: 18px;
1975
  }
1976
 
 
1978
  padding: 6px 15px;
1979
  font-size: 12px;
1980
  }
1981
+
1982
+ #aiChatContainer {
1983
+ width: 100%;
1984
+ }
1985
  }
1986
  </style>
1987
  </head>
 
1992
  <div class="title">ν™ˆμœΌλ‘œ λŒμ•„κ°€κΈ°</div>
1993
  </div>
1994
 
1995
+ <!-- AI λ²„νŠΌ μΆ”κ°€ -->
1996
+ <div id="aiButton" class="floating-ai" style="display:none;">
1997
+ <div class="icon"><i class="fas fa-robot"></i></div>
1998
+ <div class="title">AI μ–΄μ‹œμŠ€ν„΄νŠΈ</div>
1999
+ </div>
2000
+
2001
+ <!-- AI 챗봇 μ»¨ν…Œμ΄λ„ˆ -->
2002
+ <div id="aiChatContainer">
2003
+ <div id="aiChatHeader">
2004
+ <h3><i class="fas fa-robot"></i> AI μ–΄μ‹œμŠ€ν„΄νŠΈ</h3>
2005
+ <button id="aiChatClose"><i class="fas fa-times"></i></button>
2006
+ </div>
2007
+ <div id="aiChatMessages"></div>
2008
+ <form id="aiChatForm">
2009
+ <input type="text" id="aiChatInput" placeholder="PDF에 λŒ€ν•΄ μ§ˆλ¬Έν•˜μ„Έμš”..." autocomplete="off">
2010
+ <button type="submit" id="aiChatSubmit"><i class="fas fa-paper-plane"></i></button>
2011
+ </form>
2012
+ </div>
2013
+
2014
  <!-- κ΄€λ¦¬μž λ²„νŠΌ -->
2015
  <div id="adminButton">
2016
  <i class="fas fa-cog"></i> Admin
 
2088
  let audioInitialized = false;
2089
  let audioContext = null;
2090
 
2091
+ // AI 챗봇 κ΄€λ ¨ λ³€μˆ˜
2092
+ let isAiChatActive = false;
2093
+ let isAiProcessing = false;
2094
+ let hasLoadedSummary = false;
2095
+
2096
  // μ˜€λ””μ˜€ μ΄ˆκΈ°ν™” ν•¨μˆ˜
2097
  function initializeAudio() {
2098
  if (audioInitialized) return Promise.resolve();
 
2190
  /* ── μœ ν‹Έ ── */
2191
  function $id(id){return document.getElementById(id)}
2192
 
2193
+ // ν˜„μž¬ μ‹œκ°„μ„ ν¬λ§·νŒ…ν•˜λŠ” ν•¨μˆ˜
2194
+ function formatTime() {
2195
+ const now = new Date();
2196
+ const hours = now.getHours().toString().padStart(2, '0');
2197
+ const minutes = now.getMinutes().toString().padStart(2, '0');
2198
+ return `${hours}:${minutes}`;
2199
+ }
2200
+
2201
+ // AI 챗봇 λ©”μ‹œμ§€ μΆ”κ°€ ν•¨μˆ˜
2202
+ function addChatMessage(content, isUser = false) {
2203
+ const messagesContainer = $id('aiChatMessages');
2204
+ const messageElement = document.createElement('div');
2205
+ messageElement.className = `chat-message ${isUser ? 'user' : 'ai'}`;
2206
+
2207
+ const currentTime = formatTime();
2208
+
2209
+ messageElement.innerHTML = `
2210
+ <div class="chat-avatar">
2211
+ <i class="fas ${isUser ? 'fa-user' : 'fa-robot'}"></i>
2212
+ </div>
2213
+ <div class="chat-bubble">
2214
+ <div class="chat-content">${content}</div>
2215
+ <div class="chat-time">${currentTime}</div>
2216
+ </div>
2217
+ `;
2218
+
2219
+ messagesContainer.appendChild(messageElement);
2220
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
2221
+ return messageElement;
2222
+ }
2223
+
2224
+ // λ‘œλ”© ν‘œμ‹œκΈ° μΆ”κ°€ ν•¨μˆ˜
2225
+ function addTypingIndicator() {
2226
+ const messagesContainer = $id('aiChatMessages');
2227
+ const indicatorElement = document.createElement('div');
2228
+ indicatorElement.className = 'typing-indicator';
2229
+ indicatorElement.innerHTML = `
2230
+ <div class="chat-avatar">
2231
+ <i class="fas fa-robot"></i>
2232
+ </div>
2233
+ <div>
2234
+ <span></span>
2235
+ <span></span>
2236
+ <span></span>
2237
+ </div>
2238
+ `;
2239
+ messagesContainer.appendChild(indicatorElement);
2240
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
2241
+ return indicatorElement;
2242
+ }
2243
+
2244
+ // AI 챗봇 ν† κΈ€ ν•¨μˆ˜
2245
+ function toggleAiChat(show = true) {
2246
+ const aiChatContainer = $id('aiChatContainer');
2247
+
2248
+ if (show) {
2249
+ // 챗봇 ν‘œμ‹œ
2250
+ aiChatContainer.style.display = 'flex';
2251
+ setTimeout(() => {
2252
+ aiChatContainer.classList.add('active');
2253
+ }, 10);
2254
+ isAiChatActive = true;
2255
+
2256
+ // 처음 μ—΄ λ•Œ μžλ™ μš”μ•½ λ‘œλ“œ
2257
+ if (!hasLoadedSummary && currentPdfId) {
2258
+ loadPdfSummary();
2259
+ }
2260
+ } else {
2261
+ // 챗봇 숨기기
2262
+ aiChatContainer.classList.remove('active');
2263
+ setTimeout(() => {
2264
+ aiChatContainer.style.display = 'none';
2265
+ }, 300);
2266
+ isAiChatActive = false;
2267
+ }
2268
+ }
2269
+
2270
+ // PDF μš”μ•½ λ‘œλ“œ ν•¨μˆ˜
2271
+ async function loadPdfSummary() {
2272
+ if (!currentPdfId || isAiProcessing || hasLoadedSummary) return;
2273
+
2274
+ try {
2275
+ isAiProcessing = true;
2276
+ const typingIndicator = addTypingIndicator();
2277
+
2278
+ // μ„œλ²„μ— μš”μ•½ μš”μ²­
2279
+ const response = await fetch(`/api/ai/summarize-pdf/${currentPdfId}`);
2280
+ const data = await response.json();
2281
+
2282
+ // λ‘œλ”© ν‘œμ‹œκΈ° 제거
2283
+ typingIndicator.remove();
2284
+
2285
+ if (data.error) {
2286
+ addChatMessage(`μš”μ•½μ„ μƒμ„±ν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${data.error}`);
2287
+ } else {
2288
+ // ν™˜μ˜ λ©”μ‹œμ§€μ™€ μš”μ•½ μΆ”κ°€
2289
+ addChatMessage(`μ•ˆλ…•ν•˜μ„Έμš”! 이 PDF에 λŒ€ν•΄ μ–΄λ–€ 것이든 μ§ˆλ¬Έν•΄μ£Όμ„Έμš”. μ œκ°€ λ„μ™€λ“œλ¦¬κ² μŠ΅λ‹ˆλ‹€.<br><br><strong>PDF μš”μ•½:</strong><br>${data.summary}`);
2290
+ hasLoadedSummary = true;
2291
+ }
2292
+ } catch (error) {
2293
+ console.error("PDF μš”μ•½ λ‘œλ“œ 였λ₯˜:", error);
2294
+ addChatMessage("PDF μš”μ•½μ„ λ‘œλ“œν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.");
2295
+ } finally {
2296
+ isAiProcessing = false;
2297
+ }
2298
+ }
2299
+
2300
+ // 질문 제좜 ν•¨μˆ˜
2301
+ async function submitQuestion(question) {
2302
+ if (!currentPdfId || isAiProcessing || !question.trim()) return;
2303
+
2304
+ try {
2305
+ isAiProcessing = true;
2306
+ $id('aiChatSubmit').disabled = true;
2307
+
2308
+ // μ‚¬μš©μž λ©”μ‹œμ§€ μΆ”κ°€
2309
+ addChatMessage(question, true);
2310
+
2311
+ // λ‘œλ”© ν‘œμ‹œκΈ° μΆ”κ°€
2312
+ const typingIndicator = addTypingIndicator();
2313
+
2314
+ // μ„œλ²„μ— 질의 μš”μ²­
2315
+ const response = await fetch(`/api/ai/query-pdf/${currentPdfId}`, {
2316
+ method: 'POST',
2317
+ headers: {
2318
+ 'Content-Type': 'application/json'
2319
+ },
2320
+ body: JSON.stringify({ query: question })
2321
+ });
2322
+
2323
+ const data = await response.json();
2324
+
2325
+ // λ‘œλ”© ν‘œμ‹œκΈ° 제거
2326
+ typingIndicator.remove();
2327
+
2328
+ if (data.error) {
2329
+ addChatMessage(`μ£„μ†‘ν•©λ‹ˆλ‹€. μ§ˆλ¬Έμ— λ‹΅λ³€ν•˜λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${data.error}`);
2330
+ } else {
2331
+ // AI 응닡 μΆ”κ°€ (λ§ˆν¬λ‹€μš΄ 처리 λ“± ν•„μš”μ‹œ μΆ”κ°€)
2332
+ addChatMessage(data.answer);
2333
+ }
2334
+ } catch (error) {
2335
+ console.error("질문 제좜 였λ₯˜:", error);
2336
+ addChatMessage("μ£„μ†‘ν•©λ‹ˆλ‹€. μ„œλ²„μ™€ 톡신 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆοΏ½οΏ½. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.");
2337
+ } finally {
2338
+ isAiProcessing = false;
2339
+ $id('aiChatSubmit').disabled = false;
2340
+ $id('aiChatInput').value = '';
2341
+ $id('aiChatInput').focus();
2342
+ }
2343
+ }
2344
+
2345
  // DOM이 λ‘œλ“œλ˜λ©΄ μ‹€ν–‰
2346
  document.addEventListener('DOMContentLoaded', function() {
2347
  console.log("DOM λ‘œλ“œ μ™„λ£Œ, 이벀트 μ„€μ • μ‹œμž‘");
 
2403
  $id('loadingPages').style.display = 'none';
2404
  currentLoadingPdfPath = null;
2405
  currentPdfId = null;
2406
+
2407
+ // AI 챗봇 λ‹«κΈ°
2408
+ toggleAiChat(false);
2409
+ hasLoadedSummary = false; // μš”μ•½ λ‘œλ“œ μƒνƒœ μ΄ˆκΈ°ν™”
2410
+ });
2411
+ }
2412
+
2413
+ // AI λ²„νŠΌ 이벀트 μ„€μ •
2414
+ const aiButton = document.getElementById('aiButton');
2415
+ if (aiButton) {
2416
+ aiButton.addEventListener('click', function() {
2417
+ toggleAiChat(!isAiChatActive);
2418
+ });
2419
+ }
2420
+
2421
+ // AI 챗봇 λ‹«κΈ° λ²„νŠΌ
2422
+ const aiChatClose = document.getElementById('aiChatClose');
2423
+ if (aiChatClose) {
2424
+ aiChatClose.addEventListener('click', function() {
2425
+ toggleAiChat(false);
2426
+ });
2427
+ }
2428
+
2429
+ // AI 챗봇 폼 제좜
2430
+ const aiChatForm = document.getElementById('aiChatForm');
2431
+ if (aiChatForm) {
2432
+ aiChatForm.addEventListener('submit', function(e) {
2433
+ e.preventDefault();
2434
+ const inputField = document.getElementById('aiChatInput');
2435
+ const question = inputField.value.trim();
2436
+
2437
+ if (question && !isAiProcessing) {
2438
+ submitQuestion(question);
2439
+ }
2440
  });
2441
  }
2442
  });
 
2706
  createFlipBook(cachedData.pages);
2707
  // ν˜„μž¬ μ—΄λ¦° PDF의 ID μ €μž₯
2708
  currentPdfId = pdfId;
2709
+ // AI λ²„νŠΌ ν‘œμ‹œ
2710
+ $id('aiButton').style.display = 'block';
2711
  return;
2712
  }
2713
  } catch (error) {
 
2747
 
2748
  // ν˜„μž¬ μ—΄λ¦° PDF의 ID μ €μž₯
2749
  currentPdfId = pdfId;
2750
+ // AI λ²„νŠΌ ν‘œμ‹œ
2751
+ $id('aiButton').style.display = 'block';
2752
  }
2753
  } catch (error) {
2754
  console.error("PDF ID둜 μ—΄κΈ° μ‹€νŒ¨:", error);
 
2787
  const card = document.querySelectorAll('.card')[i];
2788
  if (card && card.dataset.pdfId) {
2789
  currentPdfId = card.dataset.pdfId;
2790
+ // AI λ²„νŠΌ ν‘œμ‹œ
2791
+ $id('aiButton').style.display = 'block';
2792
  } else {
2793
  currentPdfId = null;
2794
+ // AI λ²„νŠΌ μˆ¨κΉ€
2795
+ $id('aiButton').style.display = 'none';
2796
  }
2797
 
2798
+ // AI 챗봇 μ΄ˆκΈ°ν™”
2799
+ toggleAiChat(false);
2800
+ hasLoadedSummary = false;
2801
+ $id('aiChatMessages').innerHTML = '';
2802
+
2803
  // κΈ°μ‘΄ FlipBook 정리
2804
  if(fb) {
2805
  fb.destroy();
 
3134
  $id('homeButton').style.display=showHome?'none':'block';
3135
  $id('adminPage').style.display='none';
3136
 
3137
+ // AI λ²„νŠΌ 관리
3138
+ $id('aiButton').style.display = (!showHome && currentPdfId) ? 'block' : 'none';
3139
+
3140
+ // AI 챗봇이 μ—΄λ €μžˆμœΌλ©΄ λ‹«κΈ°
3141
+ if (isAiChatActive) {
3142
+ toggleAiChat(false);
3143
+ }
3144
+
3145
  // λ·°μ–΄ λͺ¨λ“œμΌ λ•Œ μŠ€νƒ€μΌ λ³€κ²½
3146
  if(!showHome) {
3147
  document.body.classList.add('viewer-mode');