Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -49,7 +49,19 @@ ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # 환경 변수에서 가져오
|
|
| 49 |
|
| 50 |
# OpenAI API 키 설정
|
| 51 |
OPENAI_API_KEY = os.getenv("LLM_API", "")
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
# 전역 캐시 객체
|
| 55 |
pdf_cache: Dict[str, Dict[str, Any]] = {}
|
|
@@ -224,8 +236,16 @@ async def get_pdf_embedding(pdf_id: str) -> Dict[str, Any]:
|
|
| 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:
|
|
@@ -234,7 +254,6 @@ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
|
| 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:
|
|
@@ -249,37 +268,66 @@ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
|
| 249 |
|
| 250 |
# gpt-4.1-mini 모델 사용
|
| 251 |
try:
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 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 |
-
|
| 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"]])
|
|
@@ -291,29 +339,54 @@ async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
|
|
| 291 |
|
| 292 |
# OpenAI API 호출
|
| 293 |
try:
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
"summary": summary,
|
| 307 |
-
"pdf_id": pdf_id
|
| 308 |
-
}
|
| 309 |
except Exception as api_error:
|
| 310 |
-
logger.error(f"OpenAI API 호출 오류: {api_error}")
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
except Exception as e:
|
| 314 |
logger.error(f"PDF 요약 생성 오류: {e}")
|
| 315 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 316 |
|
|
|
|
| 317 |
# 최적화된 PDF 페이지 캐싱 함수
|
| 318 |
async def cache_pdf(pdf_path: str):
|
| 319 |
try:
|
|
@@ -2268,79 +2341,104 @@ HTML = """
|
|
| 2268 |
}
|
| 2269 |
|
| 2270 |
// PDF 요약 로드 함수
|
| 2271 |
-
|
| 2272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2273 |
|
| 2274 |
-
|
| 2275 |
-
|
| 2276 |
-
|
| 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 |
-
|
| 2302 |
-
|
| 2303 |
-
|
| 2304 |
-
|
| 2305 |
-
|
| 2306 |
-
|
| 2307 |
-
|
| 2308 |
-
|
| 2309 |
-
|
| 2310 |
-
|
| 2311 |
-
|
| 2312 |
-
|
| 2313 |
-
|
| 2314 |
-
|
| 2315 |
-
|
| 2316 |
-
|
| 2317 |
-
|
| 2318 |
-
|
| 2319 |
-
|
| 2320 |
-
|
| 2321 |
-
|
| 2322 |
-
|
| 2323 |
-
|
| 2324 |
-
|
| 2325 |
-
|
| 2326 |
-
|
| 2327 |
-
|
| 2328 |
-
|
| 2329 |
-
|
| 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() {
|
|
|
|
| 49 |
|
| 50 |
# OpenAI API 키 설정
|
| 51 |
OPENAI_API_KEY = os.getenv("LLM_API", "")
|
| 52 |
+
# API 키가 없거나 비어있을 때 플래그 설정
|
| 53 |
+
HAS_VALID_API_KEY = bool(OPENAI_API_KEY and OPENAI_API_KEY.strip())
|
| 54 |
+
|
| 55 |
+
if HAS_VALID_API_KEY:
|
| 56 |
+
try:
|
| 57 |
+
openai_client = OpenAI(api_key=OPENAI_API_KEY, timeout=30.0)
|
| 58 |
+
logger.info("OpenAI 클라이언트 초기화 성공")
|
| 59 |
+
except Exception as e:
|
| 60 |
+
logger.error(f"OpenAI 클라이언트 초기화 실패: {e}")
|
| 61 |
+
HAS_VALID_API_KEY = False
|
| 62 |
+
else:
|
| 63 |
+
logger.warning("유효한 OpenAI API 키가 없습니다. AI 기능이 제한됩니다.")
|
| 64 |
+
openai_client = None
|
| 65 |
|
| 66 |
# 전역 캐시 객체
|
| 67 |
pdf_cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
| 236 |
return {"error": str(e), "pdf_id": pdf_id}
|
| 237 |
|
| 238 |
# PDF 내용 기반 질의응답
|
| 239 |
+
# PDF 내용 기반 질의응답 함수 개선
|
| 240 |
async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
| 241 |
try:
|
| 242 |
+
# API 키가 없거나 유효하지 않은 경우
|
| 243 |
+
if not HAS_VALID_API_KEY or not openai_client:
|
| 244 |
+
return {
|
| 245 |
+
"error": "OpenAI API 키가 설정되지 않았습니다.",
|
| 246 |
+
"answer": "죄송합니다. 현재 AI 기능이 비활성화되어 있어 질문에 답변할 수 없습니다. 시스템 관리자에게 문의하세요."
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
# 임베딩 데이터 가져오기
|
| 250 |
embedding_data = await get_pdf_embedding(pdf_id)
|
| 251 |
if "error" in embedding_data:
|
|
|
|
| 254 |
# 청크 텍스트 모으기 (임시로 간단하게 전체 텍스트 사용)
|
| 255 |
all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
|
| 256 |
|
|
|
|
| 257 |
# 컨텍스트 크기를 고려하여 텍스트가 너무 길면 앞부분만 사용
|
| 258 |
max_context_length = 60000 # 토큰 수가 아닌 문자 수 기준 (대략적인 제한)
|
| 259 |
if len(all_text) > max_context_length:
|
|
|
|
| 268 |
|
| 269 |
# gpt-4.1-mini 모델 사용
|
| 270 |
try:
|
| 271 |
+
# 타임아웃 및 재시도 설정 개선
|
| 272 |
+
for attempt in range(3): # 최대 3번 재시도
|
| 273 |
+
try:
|
| 274 |
+
response = openai_client.chat.completions.create(
|
| 275 |
+
model="gpt-4.1-mini",
|
| 276 |
+
messages=[
|
| 277 |
+
{"role": "system", "content": system_prompt},
|
| 278 |
+
{"role": "user", "content": f"다음 PDF 내용을 참고하여 질문에 답변해주세요.\n\nPDF 내용:\n{all_text}\n\n질문: {query}"}
|
| 279 |
+
],
|
| 280 |
+
temperature=0.7,
|
| 281 |
+
max_tokens=2048,
|
| 282 |
+
timeout=30.0 # 30초 타임아웃
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
answer = response.choices[0].message.content
|
| 286 |
+
return {
|
| 287 |
+
"answer": answer,
|
| 288 |
+
"pdf_id": pdf_id,
|
| 289 |
+
"query": query
|
| 290 |
+
}
|
| 291 |
+
except Exception as api_error:
|
| 292 |
+
logger.error(f"OpenAI API 호출 오류 (시도 {attempt+1}/3): {api_error}")
|
| 293 |
+
if attempt == 2: # 마지막 시도에서도 실패
|
| 294 |
+
raise api_error
|
| 295 |
+
await asyncio.sleep(1 * (attempt + 1)) # 재시도 간 지연 시간 증가
|
| 296 |
|
| 297 |
+
# 여기까지 도달하지 않아야 함
|
| 298 |
+
raise Exception("API 호출 재시도 모두 실패")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
except Exception as api_error:
|
| 300 |
+
logger.error(f"OpenAI API 호출 최종 오류: {api_error}")
|
| 301 |
+
# 오류 유형에 따른 더 명확한 메시지 제공
|
| 302 |
+
error_message = str(api_error)
|
| 303 |
+
if "Connection" in error_message:
|
| 304 |
+
return {"error": "OpenAI 서버와 연결할 수 없습니다. 인터넷 연결을 확인하세요."}
|
| 305 |
+
elif "Unauthorized" in error_message or "Authentication" in error_message:
|
| 306 |
+
return {"error": "API 키가 유효하지 않습니다."}
|
| 307 |
+
elif "Rate limit" in error_message:
|
| 308 |
+
return {"error": "API 호출 한도를 초과했습니다. 잠시 후 다시 시도하세요."}
|
| 309 |
+
else:
|
| 310 |
+
return {"error": f"AI 응답 생성 중 오류가 발생했습니다: {error_message}"}
|
| 311 |
+
|
| 312 |
except Exception as e:
|
| 313 |
logger.error(f"질의응답 처리 오류: {e}")
|
| 314 |
return {"error": str(e)}
|
| 315 |
|
| 316 |
# PDF 요약 생성
|
| 317 |
+
# PDF 요약 생성 함수 개선
|
| 318 |
async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
|
| 319 |
try:
|
| 320 |
+
# API 키가 없거나 유효하지 않은 경우
|
| 321 |
+
if not HAS_VALID_API_KEY or not openai_client:
|
| 322 |
+
return {
|
| 323 |
+
"error": "OpenAI API 키가 설정되지 않았습니다. 'LLM_API' 환경 변수를 확인하세요.",
|
| 324 |
+
"summary": "API 키가 없어 요약을 생성할 수 없습니다. 시스템 관리자에게 문의하세요."
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
# 임베딩 데이터 가져오기
|
| 328 |
embedding_data = await get_pdf_embedding(pdf_id)
|
| 329 |
if "error" in embedding_data:
|
| 330 |
+
return {"error": embedding_data["error"], "summary": "PDF에서 텍스트를 추출할 수 없습니다."}
|
| 331 |
|
| 332 |
# 청크 텍스트 모으기 (제한된 길이)
|
| 333 |
all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
|
|
|
|
| 339 |
|
| 340 |
# OpenAI API 호출
|
| 341 |
try:
|
| 342 |
+
# 타임아웃 및 재시도 설정 개선
|
| 343 |
+
for attempt in range(3): # 최대 3번 재시도
|
| 344 |
+
try:
|
| 345 |
+
response = openai_client.chat.completions.create(
|
| 346 |
+
model="gpt-4.1-mini",
|
| 347 |
+
messages=[
|
| 348 |
+
{"role": "system", "content": "다음 PDF 내용을 간결하게 요약해주세요. 핵심 주제와 주요 포인트를 포함한 요약을 500자 이내로 작성해주세요."},
|
| 349 |
+
{"role": "user", "content": f"PDF 내용:\n{all_text}"}
|
| 350 |
+
],
|
| 351 |
+
temperature=0.7,
|
| 352 |
+
max_tokens=1024,
|
| 353 |
+
timeout=30.0 # 30초 타임아웃
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
summary = response.choices[0].message.content
|
| 357 |
+
return {
|
| 358 |
+
"summary": summary,
|
| 359 |
+
"pdf_id": pdf_id
|
| 360 |
+
}
|
| 361 |
+
except Exception as api_error:
|
| 362 |
+
logger.error(f"OpenAI API 호출 오류 (시도 {attempt+1}/3): {api_error}")
|
| 363 |
+
if attempt == 2: # 마지막 시도에서도 실패
|
| 364 |
+
raise api_error
|
| 365 |
+
await asyncio.sleep(1 * (attempt + 1)) # 재시도 간 지연 시간 증가
|
| 366 |
|
| 367 |
+
# 여기까지 도달하지 않아야 함
|
| 368 |
+
raise Exception("API 호출 재시도 모두 실패")
|
|
|
|
|
|
|
|
|
|
| 369 |
except Exception as api_error:
|
| 370 |
+
logger.error(f"OpenAI API 호출 최종 오류: {api_error}")
|
| 371 |
+
# 오류 유형에 따른 더 명확한 메시지 제공
|
| 372 |
+
error_message = str(api_error)
|
| 373 |
+
if "Connection" in error_message:
|
| 374 |
+
return {"error": "OpenAI 서버와 연결할 수 없습니다. 인터넷 연결을 확인하세요.", "pdf_id": pdf_id}
|
| 375 |
+
elif "Unauthorized" in error_message or "Authentication" in error_message:
|
| 376 |
+
return {"error": "API 키가 유효하지 않습니다.", "pdf_id": pdf_id}
|
| 377 |
+
elif "Rate limit" in error_message:
|
| 378 |
+
return {"error": "API 호출 한도를 초과했습니다. 잠시 후 다시 시도하세요.", "pdf_id": pdf_id}
|
| 379 |
+
else:
|
| 380 |
+
return {"error": f"AI 요약 생성 중 오류가 발생했습니다: {error_message}", "pdf_id": pdf_id}
|
| 381 |
|
| 382 |
except Exception as e:
|
| 383 |
logger.error(f"PDF 요약 생성 오류: {e}")
|
| 384 |
+
return {
|
| 385 |
+
"error": str(e),
|
| 386 |
+
"summary": "PDF 요약 중 오류가 발생했습니다. PDF 페이지 수가 너무 많거나 형식이 지원되지 않을 수 있습니다."
|
| 387 |
+
}
|
| 388 |
|
| 389 |
+
|
| 390 |
# 최적화된 PDF 페이지 캐싱 함수
|
| 391 |
async def cache_pdf(pdf_path: str):
|
| 392 |
try:
|
|
|
|
| 2341 |
}
|
| 2342 |
|
| 2343 |
// PDF 요약 로드 함수
|
| 2344 |
+
// PDF 요약 로드 함수
|
| 2345 |
+
async function loadPdfSummary() {
|
| 2346 |
+
if (!currentPdfId || isAiProcessing || hasLoadedSummary) return;
|
| 2347 |
+
|
| 2348 |
+
try {
|
| 2349 |
+
isAiProcessing = true;
|
| 2350 |
+
const typingIndicator = addTypingIndicator();
|
| 2351 |
+
|
| 2352 |
+
// 서버에 요약 요청
|
| 2353 |
+
const response = await fetch(`/api/ai/summarize-pdf/${currentPdfId}`);
|
| 2354 |
+
const data = await response.json();
|
| 2355 |
+
|
| 2356 |
+
// 로딩 표시기 제거
|
| 2357 |
+
typingIndicator.remove();
|
| 2358 |
+
|
| 2359 |
+
if (data.error) {
|
| 2360 |
+
// 오류 메시지 표시
|
| 2361 |
+
addChatMessage(`PDF 요약을 생성하는 중 문제가 발생했습니다: ${data.error}<br><br>계속 질문을 입력하시면 PDF 내용을 기반으로 답변을 시도하겠습니다.`);
|
| 2362 |
|
| 2363 |
+
// 요약이 실패해도 특정 경우에는 사용자에게 알리고 계속 사용 가능하도록 설정
|
| 2364 |
+
if (data.summary) {
|
| 2365 |
+
addChatMessage(`<strong>PDF에서 추출한 정보:</strong><br>${data.summary}`);
|
| 2366 |
+
hasLoadedSummary = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2367 |
}
|
| 2368 |
+
} else {
|
| 2369 |
+
// 환영 메시지와 요약 추가
|
| 2370 |
+
addChatMessage(`안녕하세요! 이 PDF에 대해 어떤 것이든 질문해주세요. 제가 도와드리겠습니다.<br><br><strong>PDF 요약:</strong><br>${data.summary}`);
|
| 2371 |
+
hasLoadedSummary = true;
|
| 2372 |
}
|
| 2373 |
+
} catch (error) {
|
| 2374 |
+
console.error("PDF 요약 로드 오류:", error);
|
| 2375 |
+
addChatMessage(`PDF 요약을 로드하는 중 오류가 발생했습니다. 서버 연결을 확인해주세요.<br><br>어떤 질문이든 입력하시면 최선을 다해 답변하겠습니다.`);
|
| 2376 |
+
} finally {
|
| 2377 |
+
isAiProcessing = false;
|
| 2378 |
+
}
|
| 2379 |
+
}
|
| 2380 |
+
|
| 2381 |
+
// 질문 제출 함수
|
| 2382 |
+
async function submitQuestion(question) {
|
| 2383 |
+
if (!currentPdfId || isAiProcessing || !question.trim()) return;
|
| 2384 |
+
|
| 2385 |
+
try {
|
| 2386 |
+
isAiProcessing = true;
|
| 2387 |
+
$id('aiChatSubmit').disabled = true;
|
| 2388 |
|
| 2389 |
+
// 사용자 메시지 추가
|
| 2390 |
+
addChatMessage(question, true);
|
| 2391 |
+
|
| 2392 |
+
// 로딩 표시기 추가
|
| 2393 |
+
const typingIndicator = addTypingIndicator();
|
| 2394 |
+
|
| 2395 |
+
// 서버에 질의 요청
|
| 2396 |
+
const response = await fetch(`/api/ai/query-pdf/${currentPdfId}`, {
|
| 2397 |
+
method: 'POST',
|
| 2398 |
+
headers: {
|
| 2399 |
+
'Content-Type': 'application/json'
|
| 2400 |
+
},
|
| 2401 |
+
body: JSON.stringify({ query: question }),
|
| 2402 |
+
// 타임아웃 설정 추가
|
| 2403 |
+
signal: AbortSignal.timeout(60000) // 60초 타임아웃
|
| 2404 |
+
});
|
| 2405 |
+
|
| 2406 |
+
const data = await response.json();
|
| 2407 |
+
|
| 2408 |
+
// 로딩 표시기 제거
|
| 2409 |
+
typingIndicator.remove();
|
| 2410 |
+
|
| 2411 |
+
if (data.error) {
|
| 2412 |
+
// 오류 메시지에 따라 다른 친절한 안내 제공
|
| 2413 |
+
if (data.error.includes("API 키")) {
|
| 2414 |
+
addChatMessage("죄송합니다. 현재 AI 서비스에 연결할 수 없습니다. 시스템 관리자에게 API 키 설정을 확인해달라고 요청해주세요.");
|
| 2415 |
+
} else if (data.error.includes("연결")) {
|
| 2416 |
+
addChatMessage("죄송합니다. AI 서비스에 연결할 수 없습니다. 인터넷 연결을 확인하거나 잠시 후 다시 시도해주세요.");
|
| 2417 |
+
} else {
|
| 2418 |
+
addChatMessage(`죄송합니다. 질문에 답변하는 중 문제가 발생했습니다: ${data.error}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2419 |
}
|
| 2420 |
+
} else {
|
| 2421 |
+
// AI 응답 추가
|
| 2422 |
+
addChatMessage(data.answer);
|
| 2423 |
+
}
|
| 2424 |
+
} catch (error) {
|
| 2425 |
+
console.error("질문 제출 오류:", error);
|
| 2426 |
+
if (error.name === 'AbortError') {
|
| 2427 |
+
addChatMessage("죄송합니다. 응답 시간이 너무 오래 걸려 요청이 취소되었습니다. 인터넷 연결을 확인하거나 더 짧은 질문으로 다시 시도해보세요.");
|
| 2428 |
+
} else {
|
| 2429 |
+
addChatMessage("죄송합니다. 서버와 통신 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
|
| 2430 |
}
|
| 2431 |
+
} finally {
|
| 2432 |
+
isAiProcessing = false;
|
| 2433 |
+
$id('aiChatSubmit').disabled = false;
|
| 2434 |
+
$id('aiChatInput').value = '';
|
| 2435 |
+
$id('aiChatInput').focus();
|
| 2436 |
+
}
|
| 2437 |
+
}
|
| 2438 |
+
|
| 2439 |
+
|
| 2440 |
+
|
| 2441 |
+
|
| 2442 |
|
| 2443 |
// DOM이 로드되면 실행
|
| 2444 |
document.addEventListener('DOMContentLoaded', function() {
|