📄 PDF 본문 검색 및 미리보기 완성
🔍 PDF/HTML 본문 검색 개선: - PDF OCR 데이터 전체 텍스트 검색 (BeautifulSoup + PyPDF2) - 서적 HTML 파일 본문 검색 지원 - 파일 타입 구분 (PDF/HTML/PDF직접추출) - 검색어 매치 횟수 기반 관련성 점수 - 절대/상대 경로 처리 개선 📱 PDF 미리보기 기능: - 검색 결과에서 PDF 직접 미리보기 (iframe) - PDF에서 검색 버튼으로 페이지 이동 - 검색어 위치 기반 뷰어 연동 - PDF 로드 실패 시 fallback UI 🎯 백엔드 API 추가: - GET /documents/{id}/pdf: PDF 파일 직접 제공 - GET /documents/{id}/search-in-content: 문서 내 검색 - 페이지별 검색 결과 및 컨텍스트 제공 - 권한 확인 및 에러 처리 🎨 프론트엔드 UX: - PDF/HTML 타입별 배지 표시 - 검색 통계에 본문 검색 결과 포함 - 미리보기 모달에서 PDF 뷰어 통합 - 검색어 하이라이트 및 컨텍스트 표시
This commit is contained in:
@@ -507,6 +507,173 @@ async def get_document_content(
|
||||
raise HTTPException(status_code=500, detail=f"Error reading document: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{document_id}/pdf")
|
||||
async def get_document_pdf(
|
||||
document_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""문서 PDF 파일 조회"""
|
||||
try:
|
||||
doc_uuid = UUID(document_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid document ID format")
|
||||
|
||||
# 문서 조회
|
||||
query = select(Document).where(Document.id == doc_uuid)
|
||||
result = await db.execute(query)
|
||||
document = result.scalar_one_or_none()
|
||||
|
||||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
# 권한 확인
|
||||
if not current_user.is_admin and not document.is_public and document.uploaded_by != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# PDF 파일 확인
|
||||
if not document.pdf_path:
|
||||
raise HTTPException(status_code=404, detail="PDF file not found for this document")
|
||||
|
||||
# PDF 파일 경로 처리
|
||||
import os
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
if document.pdf_path.startswith('/'):
|
||||
file_path = document.pdf_path
|
||||
else:
|
||||
file_path = os.path.join("/app/data/documents", document.pdf_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(status_code=404, detail="PDF file not found on disk")
|
||||
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
media_type='application/pdf',
|
||||
filename=f"{document.title}.pdf"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{document_id}/search-in-content")
|
||||
async def search_in_document_content(
|
||||
document_id: str,
|
||||
q: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""특정 문서 내에서 텍스트 검색 및 페이지 위치 반환"""
|
||||
try:
|
||||
doc_uuid = UUID(document_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid document ID format")
|
||||
|
||||
# 문서 조회
|
||||
query = select(Document).where(Document.id == doc_uuid)
|
||||
result = await db.execute(query)
|
||||
document = result.scalar_one_or_none()
|
||||
|
||||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
# 권한 확인
|
||||
if not current_user.is_admin and not document.is_public and document.uploaded_by != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
search_results = []
|
||||
|
||||
# HTML 파일에서 검색 (OCR 결과)
|
||||
if document.html_path:
|
||||
try:
|
||||
import os
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
|
||||
# 절대 경로 처리
|
||||
if document.html_path.startswith('/'):
|
||||
html_file_path = document.html_path
|
||||
else:
|
||||
html_file_path = os.path.join("/app/data/documents", document.html_path)
|
||||
|
||||
if os.path.exists(html_file_path):
|
||||
with open(html_file_path, 'r', encoding='utf-8') as f:
|
||||
html_content = f.read()
|
||||
|
||||
# HTML에서 페이지별로 검색
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# 페이지 구분자 찾기 (OCR 결과에서 페이지 정보)
|
||||
pages = soup.find_all(['div', 'section'], class_=re.compile(r'page|Page'))
|
||||
|
||||
if not pages:
|
||||
# 페이지 구분이 없으면 전체 텍스트에서 검색
|
||||
text_content = soup.get_text()
|
||||
matches = []
|
||||
start = 0
|
||||
while True:
|
||||
pos = text_content.lower().find(q.lower(), start)
|
||||
if pos == -1:
|
||||
break
|
||||
|
||||
# 컨텍스트 추출
|
||||
context_start = max(0, pos - 100)
|
||||
context_end = min(len(text_content), pos + len(q) + 100)
|
||||
context = text_content[context_start:context_end]
|
||||
|
||||
matches.append({
|
||||
"page": 1,
|
||||
"position": pos,
|
||||
"context": context,
|
||||
"match_text": text_content[pos:pos + len(q)]
|
||||
})
|
||||
|
||||
start = pos + 1
|
||||
if len(matches) >= 10: # 최대 10개 결과
|
||||
break
|
||||
|
||||
search_results.extend(matches)
|
||||
else:
|
||||
# 페이지별로 검색
|
||||
for page_num, page_elem in enumerate(pages, 1):
|
||||
page_text = page_elem.get_text()
|
||||
matches = []
|
||||
start = 0
|
||||
|
||||
while True:
|
||||
pos = page_text.lower().find(q.lower(), start)
|
||||
if pos == -1:
|
||||
break
|
||||
|
||||
# 컨텍스트 추출
|
||||
context_start = max(0, pos - 100)
|
||||
context_end = min(len(page_text), pos + len(q) + 100)
|
||||
context = page_text[context_start:context_end]
|
||||
|
||||
matches.append({
|
||||
"page": page_num,
|
||||
"position": pos,
|
||||
"context": context,
|
||||
"match_text": page_text[pos:pos + len(q)]
|
||||
})
|
||||
|
||||
start = pos + 1
|
||||
if len(matches) >= 5: # 페이지당 최대 5개
|
||||
break
|
||||
|
||||
search_results.extend(matches)
|
||||
|
||||
except Exception as e:
|
||||
print(f"HTML 검색 오류: {e}")
|
||||
|
||||
return {
|
||||
"document_id": document_id,
|
||||
"query": q,
|
||||
"total_matches": len(search_results),
|
||||
"matches": search_results[:20], # 최대 20개 결과
|
||||
"has_pdf": bool(document.pdf_path),
|
||||
"has_html": bool(document.html_path)
|
||||
}
|
||||
|
||||
|
||||
class UpdateDocumentRequest(BaseModel):
|
||||
"""문서 업데이트 요청"""
|
||||
title: Optional[str] = None
|
||||
|
||||
@@ -547,13 +547,21 @@ async def search_document_content(
|
||||
search_results = []
|
||||
|
||||
for doc in documents:
|
||||
# HTML 파일에서 텍스트 검색
|
||||
text_content = ""
|
||||
file_type = ""
|
||||
|
||||
# HTML 파일에서 텍스트 검색 (PDF OCR 결과 또는 서적 HTML)
|
||||
if doc.html_path:
|
||||
try:
|
||||
import os
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
html_file_path = os.path.join("/app/data/documents", doc.html_path)
|
||||
# 절대 경로 처리
|
||||
if doc.html_path.startswith('/'):
|
||||
html_file_path = doc.html_path
|
||||
else:
|
||||
html_file_path = os.path.join("/app/data/documents", doc.html_path)
|
||||
|
||||
if os.path.exists(html_file_path):
|
||||
with open(html_file_path, 'r', encoding='utf-8') as f:
|
||||
html_content = f.read()
|
||||
@@ -562,27 +570,75 @@ async def search_document_content(
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
text_content = soup.get_text()
|
||||
|
||||
# 검색어가 포함된 경우
|
||||
if query.lower() in text_content.lower():
|
||||
# 검색어 주변 컨텍스트 추출
|
||||
context = extract_search_context(text_content, query)
|
||||
# PDF인지 서적인지 구분
|
||||
if doc.pdf_path:
|
||||
file_type = "PDF"
|
||||
else:
|
||||
file_type = "HTML"
|
||||
|
||||
# 관련성 점수 계산
|
||||
score = 2.0 # 본문 매치는 높은 점수
|
||||
|
||||
search_results.append(SearchResult(
|
||||
type="document_content",
|
||||
id=str(doc.id),
|
||||
title=f"📄 {doc.title} (본문)",
|
||||
content=context,
|
||||
document_id=str(doc.id),
|
||||
document_title=doc.title,
|
||||
created_at=doc.created_at,
|
||||
relevance_score=score
|
||||
))
|
||||
except Exception as e:
|
||||
print(f"문서 본문 검색 오류: {e}")
|
||||
print(f"HTML 파일 읽기 오류 ({doc.html_path}): {e}")
|
||||
continue
|
||||
|
||||
# PDF 파일 직접 텍스트 추출 (HTML이 없는 경우)
|
||||
elif doc.pdf_path:
|
||||
try:
|
||||
import os
|
||||
import PyPDF2
|
||||
|
||||
# 절대 경로 처리
|
||||
if doc.pdf_path.startswith('/'):
|
||||
pdf_file_path = doc.pdf_path
|
||||
else:
|
||||
pdf_file_path = os.path.join("/app/data/documents", doc.pdf_path)
|
||||
|
||||
if os.path.exists(pdf_file_path):
|
||||
with open(pdf_file_path, 'rb') as f:
|
||||
pdf_reader = PyPDF2.PdfReader(f)
|
||||
text_pages = []
|
||||
|
||||
# 모든 페이지에서 텍스트 추출
|
||||
for page_num in range(len(pdf_reader.pages)):
|
||||
page = pdf_reader.pages[page_num]
|
||||
page_text = page.extract_text()
|
||||
if page_text.strip():
|
||||
text_pages.append(f"[페이지 {page_num + 1}]\n{page_text}")
|
||||
|
||||
text_content = "\n\n".join(text_pages)
|
||||
file_type = "PDF (직접추출)"
|
||||
|
||||
except Exception as e:
|
||||
print(f"PDF 파일 읽기 오류 ({doc.pdf_path}): {e}")
|
||||
continue
|
||||
|
||||
# 검색어가 포함된 경우
|
||||
if text_content and query.lower() in text_content.lower():
|
||||
# 검색어 주변 컨텍스트 추출
|
||||
context = extract_search_context(text_content, query, context_length=300)
|
||||
|
||||
# 관련성 점수 계산
|
||||
score = 2.0 # 본문 매치는 높은 점수
|
||||
|
||||
# 검색어 매치 횟수로 점수 조정
|
||||
match_count = text_content.lower().count(query.lower())
|
||||
score += min(match_count * 0.1, 1.0) # 최대 1점 추가
|
||||
|
||||
search_results.append(SearchResult(
|
||||
type="document_content",
|
||||
id=str(doc.id),
|
||||
title=f"📄 {doc.title} ({file_type} 본문)",
|
||||
content=context,
|
||||
document_id=str(doc.id),
|
||||
document_title=doc.title,
|
||||
created_at=doc.created_at,
|
||||
relevance_score=score,
|
||||
highlight_info={
|
||||
"file_type": file_type,
|
||||
"match_count": match_count,
|
||||
"has_pdf": bool(doc.pdf_path),
|
||||
"has_html": bool(doc.html_path)
|
||||
}
|
||||
))
|
||||
|
||||
return search_results
|
||||
|
||||
|
||||
Reference in New Issue
Block a user