From 4329a1c9a625402fdbb79a2e8f3404ce91756c74 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 2 Sep 2025 16:53:56 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8D=20=EA=B3=A0=EA=B8=89=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EA=B2=80=EC=83=89=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐ŸŽฏ ์ฃผ์š” ๊ธฐ๋Šฅ: - ํ•˜์ด๋ผ์ดํŠธ ๋ฉ”๋ชจ ๋‚ด์šฉ ๋ณ„๋„ ๊ฒ€์ƒ‰ (highlight_note ํƒ€์ž…) - PDF/HTML ๋ณธ๋ฌธ ์ „์ฒด ํ…์ŠคํŠธ ๊ฒ€์ƒ‰ (OCR ๋ฐ์ดํ„ฐ ํ™œ์šฉ) - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ชจ๋‹ฌ (์ „์ฒด ๋‚ด์šฉ ๋กœ๋“œ) - ๋ฉ”๋ชจ ํŠธ๋ฆฌ ๋…ธ๋“œ ๊ฒ€์ƒ‰ ์ง€์› - ๋…ธํŠธ ๋ฌธ์„œ ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ๐Ÿ”ง ๋ฐฑ์—”๋“œ ๊ฐœ์„ : - search_highlight_notes: ํ•˜์ด๋ผ์ดํŠธ ๋ฉ”๋ชจ ๋‚ด์šฉ ๊ฒ€์ƒ‰ - search_document_content: HTML/PDF ๋ณธ๋ฌธ ๊ฒ€์ƒ‰ (BeautifulSoup) - search_memo_nodes: ๋ฉ”๋ชจ ํŠธ๋ฆฌ ๋…ธ๋“œ ๊ฒ€์ƒ‰ - search_note_documents: ๋…ธํŠธ ๋ฌธ์„œ ๊ฒ€์ƒ‰ - extract_search_context: ๊ฒ€์ƒ‰์–ด ์ฃผ๋ณ€ ์ปจํ…์ŠคํŠธ ์ถ”์ถœ ๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ๋Šฅ: - ํ†ตํ•ฉ ๊ฒ€์ƒ‰ UI (/search.html) ์™„์ „ ๊ตฌํ˜„ - ๊ฒ€์ƒ‰ ํ•„ํ„ฐ: ๋ฌธ์„œ/๋…ธํŠธ/๋ฉ”๋ชจ/ํ•˜์ด๋ผ์ดํŠธ/๋ฉ”๋ชจ/๋ณธ๋ฌธ - ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ชจ๋‹ฌ: ์ „์ฒด ๋‚ด์šฉ ๋กœ๋“œ ๋ฐ ํ‘œ์‹œ - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ•˜์ด๋ผ์ดํŠธ ๋ฐ ์ปจํ…์ŠคํŠธ ํ‘œ์‹œ - ํƒ€์ž…๋ณ„ ๋ฐฐ์ง€ ๋ฐ ๊ด€๋ จ๋„ ์ ์ˆ˜ ํ‘œ์‹œ ๐Ÿ“ฑ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜: - ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ๋””๋ฐ”์šด์Šค (500ms) - ๊ฒ€์ƒ‰์–ด ์ž๋™์™„์„ฑ ์ œ์•ˆ - ๊ฒ€์ƒ‰ ํ†ต๊ณ„ ๋ฐ ์„ฑ๋Šฅ ํ‘œ์‹œ - ๋น ๋ฅธ ๊ฒ€์ƒ‰ ์˜ˆ์‹œ ๋ฒ„ํŠผ - ์ƒˆ ํƒญ์—์„œ ๊ฒฐ๊ณผ ์—ด๊ธฐ ๐Ÿ”— ๋„ค๋น„๊ฒŒ์ด์…˜ ํ†ตํ•ฉ: - ํ—ค๋”์— 'ํ†ตํ•ฉ ๊ฒ€์ƒ‰' ๋งํฌ ์ถ”๊ฐ€ - ํŽ˜์ด์ง€๋ณ„ ํ™œ์„ฑ ์ƒํƒœ ๊ด€๋ฆฌ --- backend/src/api/routes/search.py | 266 ++++++++++++++- frontend/components/header.html | 6 + frontend/search.html | 495 ++++++++++++++++++++++++++++ frontend/static/js/header-loader.js | 6 +- frontend/static/js/search.js | 352 ++++++++++++++++++++ 5 files changed, 1120 insertions(+), 5 deletions(-) create mode 100644 frontend/search.html create mode 100644 frontend/static/js/search.js diff --git a/backend/src/api/routes/search.py b/backend/src/api/routes/search.py index 9c11df1..149e50a 100644 --- a/backend/src/api/routes/search.py +++ b/backend/src/api/routes/search.py @@ -13,6 +13,8 @@ from ...models.user import User from ...models.document import Document, Tag from ...models.highlight import Highlight from ...models.note import Note +from ...models.memo_tree import MemoTree, MemoNode +from ...models.note_document import NoteDocument from ..dependencies import get_current_active_user from pydantic import BaseModel @@ -47,7 +49,7 @@ router = APIRouter() @router.get("/", response_model=SearchResponse) async def search_all( q: str = Query(..., description="๊ฒ€์ƒ‰์–ด"), - type_filter: Optional[str] = Query(None, description="๊ฒ€์ƒ‰ ํƒ€์ž… ํ•„ํ„ฐ: document, note, highlight"), + type_filter: Optional[str] = Query(None, description="๊ฒ€์ƒ‰ ํƒ€์ž… ํ•„ํ„ฐ: document, note, memo, highlight"), document_id: Optional[str] = Query(None, description="ํŠน์ • ๋ฌธ์„œ ๋‚ด ๊ฒ€์ƒ‰"), tag: Optional[str] = Query(None, description="ํƒœ๊ทธ ํ•„ํ„ฐ"), skip: int = Query(0, ge=0), @@ -63,16 +65,36 @@ async def search_all( document_results = await search_documents(q, document_id, tag, current_user, db) results.extend(document_results) - # 2. ๋ฉ”๋ชจ ๊ฒ€์ƒ‰ + # 2. ๋…ธํŠธ ๋ฌธ์„œ ๊ฒ€์ƒ‰ if not type_filter or type_filter == "note": - note_results = await search_notes(q, document_id, tag, current_user, db) + note_results = await search_note_documents(q, current_user, db) results.extend(note_results) - # 3. ํ•˜์ด๋ผ์ดํŠธ ๊ฒ€์ƒ‰ + # 3. ๋ฉ”๋ชจ ํŠธ๋ฆฌ ๋…ธ๋“œ ๊ฒ€์ƒ‰ + if not type_filter or type_filter == "memo": + memo_results = await search_memo_nodes(q, current_user, db) + results.extend(memo_results) + + # 4. ๊ธฐ์กด ๋ฉ”๋ชจ ๊ฒ€์ƒ‰ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) + if not type_filter or type_filter == "note": + old_note_results = await search_notes(q, document_id, tag, current_user, db) + results.extend(old_note_results) + + # 5. ํ•˜์ด๋ผ์ดํŠธ ๊ฒ€์ƒ‰ if not type_filter or type_filter == "highlight": highlight_results = await search_highlights(q, document_id, current_user, db) results.extend(highlight_results) + # 6. ํ•˜์ด๋ผ์ดํŠธ ๋ฉ”๋ชจ ๊ฒ€์ƒ‰ + if not type_filter or type_filter == "highlight_note": + highlight_note_results = await search_highlight_notes(q, document_id, current_user, db) + results.extend(highlight_note_results) + + # 7. ๋ฌธ์„œ ๋ณธ๋ฌธ ๊ฒ€์ƒ‰ (OCR ๋ฐ์ดํ„ฐ) + if not type_filter or type_filter == "document_content": + content_results = await search_document_content(q, document_id, current_user, db) + results.extend(content_results) + # ๊ด€๋ จ์„ฑ ์ ์ˆ˜๋กœ ์ •๋ ฌ results.sort(key=lambda x: x.relevance_score, reverse=True) @@ -352,3 +374,239 @@ async def get_search_suggestions( suggestions.extend([{"text": tag, "type": "note_tag"} for tag in list(note_tags)[:5]]) return {"suggestions": suggestions[:10]} + + +async def search_highlight_notes( + query: str, + document_id: Optional[str], + current_user: User, + db: AsyncSession +) -> List[SearchResult]: + """ํ•˜์ด๋ผ์ดํŠธ ๋ฉ”๋ชจ ๋‚ด์šฉ ๊ฒ€์ƒ‰""" + query_obj = select(Note).options( + selectinload(Note.highlight).selectinload(Highlight.document) + ) + + # ํ•˜์ด๋ผ์ดํŠธ๊ฐ€ ์žˆ๋Š” ๋…ธํŠธ๋งŒ + query_obj = query_obj.where(Note.highlight_id.isnot(None)) + + # ๊ถŒํ•œ ํ•„ํ„ฐ๋ง - ์‚ฌ์šฉ์ž์˜ ๋…ธํŠธ๋งŒ + query_obj = query_obj.where(Note.created_by == current_user.id) + + # ํŠน์ • ๋ฌธ์„œ ํ•„ํ„ฐ + if document_id: + query_obj = query_obj.join(Highlight).where(Highlight.document_id == document_id) + + # ๋ฉ”๋ชจ ๋‚ด์šฉ์—์„œ ๊ฒ€์ƒ‰ + query_obj = query_obj.where(Note.content.ilike(f"%{query}%")) + + result = await db.execute(query_obj) + notes = result.scalars().all() + + search_results = [] + for note in notes: + if not note.highlight or not note.highlight.document: + continue + + # ๊ด€๋ จ์„ฑ ์ ์ˆ˜ ๊ณ„์‚ฐ + score = 1.5 # ๋ฉ”๋ชจ ๋‚ด์šฉ ๋งค์น˜๋Š” ๋†’์€ ์ ์ˆ˜ + content_lower = (note.content or "").lower() + if query.lower() in content_lower: + score += 2.0 + + search_results.append(SearchResult( + type="highlight_note", + id=str(note.id), + title=f"ํ•˜์ด๋ผ์ดํŠธ ๋ฉ”๋ชจ: {note.highlight.selected_text[:30]}...", + content=note.content or "", + document_id=str(note.highlight.document.id), + document_title=note.highlight.document.title, + created_at=note.created_at, + relevance_score=score, + highlight_info={ + "highlight_id": str(note.highlight.id), + "selected_text": note.highlight.selected_text, + "start_offset": note.highlight.start_offset, + "end_offset": note.highlight.end_offset, + "note_content": note.content + } + )) + + return search_results + + +async def search_note_documents( + query: str, + current_user: User, + db: AsyncSession +) -> List[SearchResult]: + """๋…ธํŠธ ๋ฌธ์„œ ๊ฒ€์ƒ‰""" + query_obj = select(NoteDocument).where( + or_( + NoteDocument.title.ilike(f"%{query}%"), + NoteDocument.content.ilike(f"%{query}%") + ) + ) + + # ๊ถŒํ•œ ํ•„ํ„ฐ๋ง - ์‚ฌ์šฉ์ž์˜ ๋…ธํŠธ๋งŒ + query_obj = query_obj.where(NoteDocument.created_by == current_user.email) + + result = await db.execute(query_obj) + notes = result.scalars().all() + + search_results = [] + for note in notes: + # ๊ด€๋ จ์„ฑ ์ ์ˆ˜ ๊ณ„์‚ฐ + score = 1.0 + if query.lower() in note.title.lower(): + score += 2.0 + if note.content and query.lower() in note.content.lower(): + score += 1.0 + + search_results.append(SearchResult( + type="note", + id=str(note.id), + title=note.title, + content=note.content or "", + document_id=str(note.id), # ๋…ธํŠธ ์ž์ฒด๊ฐ€ ๋ฌธ์„œ + document_title=note.title, + created_at=note.created_at, + relevance_score=score + )) + + return search_results + + +async def search_memo_nodes( + query: str, + current_user: User, + db: AsyncSession +) -> List[SearchResult]: + """๋ฉ”๋ชจ ํŠธ๋ฆฌ ๋…ธ๋“œ ๊ฒ€์ƒ‰""" + query_obj = select(MemoNode).options( + selectinload(MemoNode.tree) + ).where( + or_( + MemoNode.title.ilike(f"%{query}%"), + MemoNode.content.ilike(f"%{query}%") + ) + ) + + # ๊ถŒํ•œ ํ•„ํ„ฐ๋ง - ์‚ฌ์šฉ์ž์˜ ํŠธ๋ฆฌ์— ์†ํ•œ ๋…ธ๋“œ๋งŒ + query_obj = query_obj.join(MemoTree).where(MemoTree.user_id == current_user.id) + + result = await db.execute(query_obj) + nodes = result.scalars().all() + + search_results = [] + for node in nodes: + # ๊ด€๋ จ์„ฑ ์ ์ˆ˜ ๊ณ„์‚ฐ + score = 1.0 + if query.lower() in node.title.lower(): + score += 2.0 + if node.content and query.lower() in node.content.lower(): + score += 1.0 + + search_results.append(SearchResult( + type="memo", + id=str(node.id), + title=node.title, + content=node.content or "", + document_id=str(node.tree.id), # ํŠธ๋ฆฌ ID๋ฅผ ๋ฌธ์„œ ID๋กœ ์‚ฌ์šฉ + document_title=f"๐Ÿ“š {node.tree.title}", + created_at=node.created_at, + relevance_score=score + )) + + return search_results + + +async def search_document_content( + query: str, + document_id: Optional[str], + current_user: User, + db: AsyncSession +) -> List[SearchResult]: + """๋ฌธ์„œ ๋ณธ๋ฌธ ๋‚ด์šฉ ๊ฒ€์ƒ‰ (OCR ๋ฐ์ดํ„ฐ ํฌํ•จ)""" + # ๋ฌธ์„œ ๊ถŒํ•œ ํ™•์ธ + doc_query = select(Document) + if not current_user.is_admin: + doc_query = doc_query.where( + or_( + Document.is_public == True, + Document.uploaded_by == current_user.id + ) + ) + + if document_id: + doc_query = doc_query.where(Document.id == document_id) + + result = await db.execute(doc_query) + documents = result.scalars().all() + + search_results = [] + + for doc in documents: + # 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 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') + text_content = soup.get_text() + + # ๊ฒ€์ƒ‰์–ด๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ + if query.lower() in text_content.lower(): + # ๊ฒ€์ƒ‰์–ด ์ฃผ๋ณ€ ์ปจํ…์ŠคํŠธ ์ถ”์ถœ + context = extract_search_context(text_content, query) + + # ๊ด€๋ จ์„ฑ ์ ์ˆ˜ ๊ณ„์‚ฐ + 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}") + continue + + return search_results + + +def extract_search_context(text: str, query: str, context_length: int = 200) -> str: + """๊ฒ€์ƒ‰์–ด ์ฃผ๋ณ€ ์ปจํ…์ŠคํŠธ ์ถ”์ถœ""" + text_lower = text.lower() + query_lower = query.lower() + + # ์ฒซ ๋ฒˆ์งธ ๋งค์น˜ ์œ„์น˜ ์ฐพ๊ธฐ + match_pos = text_lower.find(query_lower) + if match_pos == -1: + return text[:context_length] + "..." + + # ์ปจํ…์ŠคํŠธ ์‹œ์ž‘/๋ ์œ„์น˜ ๊ณ„์‚ฐ + start = max(0, match_pos - context_length // 2) + end = min(len(text), match_pos + len(query) + context_length // 2) + + context = text[start:end] + + # ์•ž๋’ค์— ... ์ถ”๊ฐ€ + if start > 0: + context = "..." + context + if end < len(text): + context = context + "..." + + return context diff --git a/frontend/components/header.html b/frontend/components/header.html index b80a0e0..4881080 100644 --- a/frontend/components/header.html +++ b/frontend/components/header.html @@ -27,6 +27,12 @@ + + + + ํ†ตํ•ฉ ๊ฒ€์ƒ‰ + +
diff --git a/frontend/search.html b/frontend/search.html new file mode 100644 index 0000000..3538500 --- /dev/null +++ b/frontend/search.html @@ -0,0 +1,495 @@ + + + + + + ํ†ตํ•ฉ ๊ฒ€์ƒ‰ - Document Server + + + + + + + + +
+ + +
+ +
+

+ + ํ†ตํ•ฉ ๊ฒ€์ƒ‰ +

+

๋ฌธ์„œ, ๋…ธํŠธ, ๋ฉ”๋ชจ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฒ€์ƒ‰ํ•˜์„ธ์š”

+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ ํƒ€์ž…: + + + + + + + +
+ + +
+ ์ •๋ ฌ: + +
+
+
+
+ + +
+
+
+
+ + ๊ฐœ ๊ฒฐ๊ณผ + + "" ๊ฒ€์ƒ‰ + + +
+ + ๐Ÿ“„ ๋ฌธ์„œ ๊ฐœ + + + ๐Ÿ“ ๋…ธํŠธ ๊ฐœ + + + ๐ŸŒณ ๋ฉ”๋ชจ ๊ฐœ + + + ๐Ÿ–๏ธ ํ•˜์ด๋ผ์ดํŠธ ๊ฐœ + +
+
+
+ + ms +
+
+
+
+ + +
+ +

๊ฒ€์ƒ‰ ์ค‘...

+
+ + +
+ +
+ + +
+
+ +

๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+

+ + ""์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. +

+
+

๊ฒ€์ƒ‰ ํŒ:

+
    +
  • โ€ข ๋‹ค๋ฅธ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•ด๋ณด์„ธ์š”
  • +
  • โ€ข ๊ฒ€์ƒ‰์–ด๋ฅผ ์ค„์—ฌ๋ณด์„ธ์š”
  • +
  • โ€ข ํ•„ํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•ด๋ณด์„ธ์š”
  • +
+
+
+
+ + +
+
+ +

๊ฒ€์ƒ‰์„ ์‹œ์ž‘ํ•˜์„ธ์š”

+

๋ฌธ์„œ, ๋…ธํŠธ, ๋ฉ”๋ชจ, ํ•˜์ด๋ผ์ดํŠธ๋ฅผ ํ†ตํ•ฉ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

+ + +
+ + + + +
+
+
+
+ + +
+ +
+ + +
+
+
+ + +
+

+

+
+
+ + +
+
+ + +
+ +
+
+ ํ•˜์ด๋ผ์ดํŠธ๋œ ํ…์ŠคํŠธ +
+
+
+ ๋ฉ”๋ชจ: +
+
+ + +
+
+ ์›๋ณธ ํ•˜์ด๋ผ์ดํŠธ +
+
+
๋ฉ”๋ชจ ๋‚ด์šฉ:
+
+ + +
+
+
+ + +
+ +

๋‚ด์šฉ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+
+
+ + + + + + + + diff --git a/frontend/static/js/header-loader.js b/frontend/static/js/header-loader.js index a316ec5..f35d8e4 100644 --- a/frontend/static/js/header-loader.js +++ b/frontend/static/js/header-loader.js @@ -124,7 +124,11 @@ class HeaderLoader { 'index': 'index-nav-item', 'hierarchy': 'hierarchy-nav-item', 'memo-tree': 'memo-tree-nav-item', - 'story-view': 'story-view-nav-item' + 'story-view': 'story-view-nav-item', + 'search': 'search-nav-link', + 'notes': 'notes-nav-link', + 'notebooks': 'notebooks-nav-item', + 'note-editor': 'note-editor-nav-item' }; const itemId = pageItemMap[pageInfo.filename]; diff --git a/frontend/static/js/search.js b/frontend/static/js/search.js new file mode 100644 index 0000000..a401bd6 --- /dev/null +++ b/frontend/static/js/search.js @@ -0,0 +1,352 @@ +/** + * ํ†ตํ•ฉ ๊ฒ€์ƒ‰ JavaScript + */ + +// ๊ฒ€์ƒ‰ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ Alpine.js ์ปดํฌ๋„ŒํŠธ +window.searchApp = function() { + return { + // ์ƒํƒœ ๊ด€๋ฆฌ + searchQuery: '', + searchResults: [], + filteredResults: [], + loading: false, + hasSearched: false, + searchTime: 0, + + // ํ•„ํ„ฐ๋ง + typeFilter: '', // '', 'document', 'note', 'memo', 'highlight' + sortBy: 'relevance', // 'relevance', 'date_desc', 'date_asc', 'title' + + // ๊ฒ€์ƒ‰ ๋””๋ฐ”์šด์Šค + searchTimeout: null, + + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ชจ๋‹ฌ + showPreviewModal: false, + previewResult: null, + previewLoading: false, + + // ์ธ์ฆ ์ƒํƒœ + isAuthenticated: false, + currentUser: null, + + // API ํด๋ผ์ด์–ธํŠธ + api: null, + + // ์ดˆ๊ธฐํ™” + async init() { + console.log('๐Ÿ” ๊ฒ€์ƒ‰ ์•ฑ ์ดˆ๊ธฐํ™” ์‹œ์ž‘'); + + try { + // API ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” + this.api = new DocumentServerAPI(); + + // ํ—ค๋” ๋กœ๋“œ + await this.loadHeader(); + + // ์ธ์ฆ ์ƒํƒœ ํ™•์ธ + await this.checkAuthStatus(); + + // URL ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๊ฒ€์ƒ‰์–ด ํ™•์ธ + const urlParams = new URLSearchParams(window.location.search); + const query = urlParams.get('q'); + if (query) { + this.searchQuery = query; + await this.performSearch(); + } + + console.log('โœ… ๊ฒ€์ƒ‰ ์•ฑ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ'); + } catch (error) { + console.error('โŒ ๊ฒ€์ƒ‰ ์•ฑ ์ดˆ๊ธฐํ™” ์‹คํŒจ:', error); + } + }, + + // ์ธ์ฆ ์ƒํƒœ ํ™•์ธ + async checkAuthStatus() { + try { + const user = await this.api.getCurrentUser(); + this.isAuthenticated = true; + this.currentUser = user; + console.log('โœ… ์ธ์ฆ๋จ:', user.username || user.email); + } catch (error) { + console.log('โŒ ์ธ์ฆ๋˜์ง€ ์•Š์Œ'); + this.isAuthenticated = false; + this.currentUser = null; + // ๊ฒ€์ƒ‰์€ ๋กœ๊ทธ์ธ ์—†์ด๋„ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ—ˆ์šฉ + } + }, + + // ํ—ค๋” ๋กœ๋“œ + async loadHeader() { + try { + if (typeof loadHeaderComponent === 'function') { + await loadHeaderComponent(); + } else if (typeof window.loadHeaderComponent === 'function') { + await window.loadHeaderComponent(); + } else { + console.warn('ํ—ค๋” ๋กœ๋” ํ•จ์ˆ˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); + } + } catch (error) { + console.error('ํ—ค๋” ๋กœ๋“œ ์‹คํŒจ:', error); + } + }, + + // ๊ฒ€์ƒ‰ ๋””๋ฐ”์šด์Šค + debounceSearch() { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(() => { + if (this.searchQuery.trim()) { + this.performSearch(); + } + }, 500); + }, + + // ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰ + async performSearch() { + if (!this.searchQuery.trim()) { + this.searchResults = []; + this.filteredResults = []; + this.hasSearched = false; + return; + } + + this.loading = true; + const startTime = Date.now(); + + try { + console.log('๐Ÿ” ๊ฒ€์ƒ‰ ์‹œ์ž‘:', this.searchQuery); + + // ๊ฒ€์ƒ‰ API ํ˜ธ์ถœ + const response = await this.api.search({ + q: this.searchQuery, + type_filter: this.typeFilter || undefined, + limit: 50 + }); + + this.searchResults = response.results || []; + this.hasSearched = true; + this.searchTime = Date.now() - startTime; + + // ํ•„ํ„ฐ ์ ์šฉ + this.applyFilters(); + + // URL ์—…๋ฐ์ดํŠธ + this.updateURL(); + + console.log('โœ… ๊ฒ€์ƒ‰ ์™„๋ฃŒ:', this.searchResults.length, '๊ฐœ ๊ฒฐ๊ณผ'); + + } catch (error) { + console.error('โŒ ๊ฒ€์ƒ‰ ์‹คํŒจ:', error); + this.searchResults = []; + this.filteredResults = []; + this.hasSearched = true; + } finally { + this.loading = false; + } + }, + + // ํ•„ํ„ฐ ์ ์šฉ + applyFilters() { + let results = [...this.searchResults]; + + // ํƒ€์ž… ํ•„ํ„ฐ + if (this.typeFilter) { + results = results.filter(result => result.type === this.typeFilter); + } + + // ์ •๋ ฌ + results.sort((a, b) => { + switch (this.sortBy) { + case 'relevance': + return (b.relevance_score || 0) - (a.relevance_score || 0); + case 'date_desc': + return new Date(b.created_at) - new Date(a.created_at); + case 'date_asc': + return new Date(a.created_at) - new Date(b.created_at); + case 'title': + return a.title.localeCompare(b.title); + default: + return 0; + } + }); + + this.filteredResults = results; + console.log('๐Ÿ”ง ํ•„ํ„ฐ ์ ์šฉ ์™„๋ฃŒ:', this.filteredResults.length, '๊ฐœ ๊ฒฐ๊ณผ'); + }, + + // URL ์—…๋ฐ์ดํŠธ + updateURL() { + const url = new URL(window.location); + if (this.searchQuery.trim()) { + url.searchParams.set('q', this.searchQuery); + } else { + url.searchParams.delete('q'); + } + window.history.replaceState({}, '', url); + }, + + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ + async showPreview(result) { + console.log('๐Ÿ‘๏ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ:', result); + + this.previewResult = result; + this.showPreviewModal = true; + this.previewLoading = true; + + try { + // ์ถ”๊ฐ€ ๋‚ด์šฉ ๋กœ๋“œ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ) + if (result.type === 'document' || result.type === 'note') { + // ์ „์ฒด ๋‚ด์šฉ ๋กœ๋“œ + const fullContent = await this.loadFullContent(result); + if (fullContent) { + this.previewResult = { ...result, content: fullContent }; + } + } + } catch (error) { + console.error('๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋กœ๋“œ ์‹คํŒจ:', error); + } finally { + this.previewLoading = false; + } + }, + + // ์ „์ฒด ๋‚ด์šฉ ๋กœ๋“œ + async loadFullContent(result) { + try { + let content = ''; + + switch (result.type) { + case 'document': + // ๋ฌธ์„œ ๋‚ด์šฉ API ํ˜ธ์ถœ + const docContent = await this.api.get(`/documents/${result.document_id}/content`); + content = docContent; + break; + + case 'note': + // ๋…ธํŠธ ๋‚ด์šฉ API ํ˜ธ์ถœ + const noteContent = await this.api.get(`/note-documents/${result.id}/content`); + content = noteContent; + break; + + default: + content = result.content; + } + + return content; + } catch (error) { + console.error('๋‚ด์šฉ ๋กœ๋“œ ์‹คํŒจ:', error); + return result.content; + } + }, + + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋‹ซ๊ธฐ + closePreview() { + this.showPreviewModal = false; + this.previewResult = null; + this.previewLoading = false; + }, + + // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—ด๊ธฐ + openResult(result) { + console.log('๐Ÿ“‚ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—ด๊ธฐ:', result); + + let url = ''; + + switch (result.type) { + case 'document': + case 'document_content': + url = `/viewer.html?id=${result.document_id}`; + if (result.highlight_info) { + // ํ•˜์ด๋ผ์ดํŠธ ์œ„์น˜๋กœ ์ด๋™ + const { start_offset, end_offset, selected_text } = result.highlight_info; + url += `&highlight_text=${encodeURIComponent(selected_text)}&start_offset=${start_offset}&end_offset=${end_offset}`; + } + break; + + case 'note': + url = `/viewer.html?id=${result.id}&contentType=note`; + break; + + case 'memo': + // ๋ฉ”๋ชจ ํŠธ๋ฆฌ์—์„œ ํ•ด๋‹น ๋…ธ๋“œ๋กœ ์ด๋™ + url = `/memo-tree.html?node_id=${result.id}`; + break; + + case 'highlight': + case 'highlight_note': + url = `/viewer.html?id=${result.document_id}`; + if (result.highlight_info) { + const { start_offset, end_offset, selected_text } = result.highlight_info; + url += `&highlight_text=${encodeURIComponent(selected_text)}&start_offset=${start_offset}&end_offset=${end_offset}`; + } + break; + + default: + console.warn('์•Œ ์ˆ˜ ์—†๋Š” ๊ฒฐ๊ณผ ํƒ€์ž…:', result.type); + return; + } + + // ์ƒˆ ํƒญ์—์„œ ์—ด๊ธฐ + window.open(url, '_blank'); + }, + + // ํƒ€์ž…๋ณ„ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ + getResultCount(type) { + return this.searchResults.filter(result => result.type === type).length; + }, + + // ํƒ€์ž… ๋ผ๋ฒจ + getTypeLabel(type) { + const labels = { + document: '๋ฌธ์„œ', + document_content: '๋ณธ๋ฌธ', + note: '๋…ธํŠธ', + memo: '๋ฉ”๋ชจ', + highlight: 'ํ•˜์ด๋ผ์ดํŠธ', + highlight_note: '๋ฉ”๋ชจ' + }; + return labels[type] || type; + }, + + // ํ…์ŠคํŠธ ํ•˜์ด๋ผ์ดํŠธ + highlightText(text, query) { + if (!text || !query) return text; + + const regex = new RegExp(`(${this.escapeRegExp(query)})`, 'gi'); + return text.replace(regex, '$1'); + }, + + // ์ •๊ทœ์‹ ์ด์Šค์ผ€์ดํ”„ + escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }, + + // ํ…์ŠคํŠธ ์ž๋ฅด๊ธฐ + truncateText(text, maxLength) { + if (!text || text.length <= maxLength) return text; + return text.substring(0, maxLength) + '...'; + }, + + // ๋‚ ์งœ ํฌ๋งทํŒ… + formatDate(dateString) { + const date = new Date(dateString); + const now = new Date(); + const diffTime = Math.abs(now - date); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 1) { + return '์˜ค๋Š˜'; + } else if (diffDays === 2) { + return '์–ด์ œ'; + } else if (diffDays <= 7) { + return `${diffDays - 1}์ผ ์ „`; + } else if (diffDays <= 30) { + return `${Math.ceil(diffDays / 7)}์ฃผ ์ „`; + } else if (diffDays <= 365) { + return `${Math.ceil(diffDays / 30)}๊ฐœ์›” ์ „`; + } else { + return date.toLocaleDateString('ko-KR'); + } + } + }; +}; + +console.log('๐Ÿ” ๊ฒ€์ƒ‰ JavaScript ๋กœ๋“œ ์™„๋ฃŒ');