feat: 노트북-서적 간 양방향 링크/백링크 시스템 완성

 주요 기능
- 노트 ↔ 서적 문서 간 양방향 링크 생성 및 이동
- 링크 대상 타입 선택 UI (서적 문서/노트북 노트)
- 통합 백링크 시스템 (일반 문서에서 노트 백링크도 표시)
- 링크 목록 UI 개선 (상세 정보 표시, 타입 구분)

🔧 백엔드 개선
- NoteLink 모델 및 API 추가 (/note-documents/{id}/links, /note-documents/{id}/backlinks)
- 일반 문서 백링크 API에서 노트 링크도 함께 조회
- target_content_type, source_content_type 필드 추가
- 노트 문서 콘텐츠 API 추가 (/note-documents/{id}/content)

🎨 프론트엔드 개선
- text-selector.html에서 노트 문서 지원
- 링크 이동 시 contentType에 따른 올바른 URL 생성
- URL 파라미터 파싱 수정 (contentType 지원)
- 링크 타입 자동 추론 로직
- 링크 목록 UI 대폭 개선 (출발점/도착점 텍스트, 타입 배지 등)

🐛 버그 수정
- 서적 목록 로드 실패 문제 해결
- 노트에서 링크 생성 시 대상 문서 열기 문제 해결
- 더미 문서로 이동하는 문제 해결
- 캐시 관련 문제 해결
This commit is contained in:
Hyungi Ahn
2025-09-02 16:22:03 +09:00
parent f711998ce9
commit d01cdeb2f5
12 changed files with 1188 additions and 82 deletions

View File

@@ -311,29 +311,45 @@
<template x-for="link in documentLinks" :key="link.id">
<div class="border rounded-lg p-4 mb-3 hover:bg-purple-50 cursor-pointer transition-colors"
@click="navigateToLink(link)">
<div class="flex items-center justify-between">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="font-medium text-purple-700 mb-1" x-text="link.target_document_title"></div>
<!-- 선택된 텍스트 또는 문서 전체 링크 -->
<div x-show="link.selected_text" class="mb-2">
<div class="text-sm text-gray-600 bg-gray-100 px-3 py-2 rounded border-l-4 border-purple-500" x-text="link.selected_text"></div>
<!-- 대상 문서 제목 -->
<div class="font-medium text-purple-700 mb-2 flex items-center">
<span x-text="link.target_document_title || link.target_note_title"></span>
<span x-show="link.target_content_type === 'note'" class="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded">노트</span>
<span x-show="link.target_content_type === 'document'" class="ml-2 text-xs bg-green-100 text-green-700 px-2 py-1 rounded">문서</span>
</div>
<div x-show="!link.selected_text" class="mb-2">
<div class="text-sm text-gray-600 italic">📄 문서 전체 링크</div>
<!-- 현재 문서에서 선택한 텍스트 (출발점) -->
<div x-show="link.selected_text" class="mb-3">
<div class="text-xs text-gray-500 mb-1">📍 현재 문서에서 선택한 텍스트:</div>
<div class="text-sm text-gray-700 bg-purple-50 px-3 py-2 rounded border-l-4 border-purple-500" x-text="link.selected_text"></div>
</div>
<!-- 대상 문서의 텍스트 (도착점) -->
<div x-show="link.target_text" class="mb-3">
<div class="text-xs text-gray-500 mb-1">🎯 대상 문서의 텍스트:</div>
<div class="text-sm text-gray-700 bg-blue-50 px-3 py-2 rounded border-l-4 border-blue-500" x-text="link.target_text"></div>
</div>
<!-- 문서 전체 링크인 경우 -->
<div x-show="!link.selected_text && !link.target_text" class="mb-3">
<div class="text-sm text-gray-600 italic bg-gray-50 px-3 py-2 rounded">📄 문서 전체 링크</div>
</div>
<!-- 설명 -->
<div x-show="link.description" class="text-sm text-gray-600 mb-2" x-text="link.description"></div>
<div x-show="link.description" class="mb-3">
<div class="text-xs text-gray-500 mb-1">💬 설명:</div>
<div class="text-sm text-gray-600 bg-yellow-50 px-3 py-2 rounded" x-text="link.description"></div>
</div>
<!-- 링크 타입과 날짜 -->
<div class="flex items-center justify-between">
<span class="text-xs text-gray-500"
x-text="link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'"></span>
<span class="text-xs text-gray-500" x-text="formatDate(link.created_at)"></span>
<div class="flex items-center justify-between text-xs text-gray-500">
<span x-text="link.link_type === 'text_fragment' ? '🔗 텍스트 조각 링크' : '📄 문서 링크'"></span>
<span x-text="formatDate(link.created_at)"></span>
</div>
</div>
<div class="ml-3">
<div class="ml-3 flex-shrink-0">
<svg class="w-5 h-5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
</svg>
@@ -376,15 +392,39 @@
<p class="text-purple-700" x-text="linkForm?.selected_text || ''"></p>
</div>
<!-- 서적 선택 -->
<!-- 링크 대상 타입 선택 -->
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-3">서적 선택</label>
<label class="block text-sm font-semibold text-gray-700 mb-3">링크 대상 타입</label>
<div class="flex space-x-4">
<label class="flex items-center">
<input type="radio"
x-model="linkForm.target_type"
value="document"
@change="onTargetTypeChange()"
class="mr-2 text-blue-600">
<span class="text-sm text-gray-700">📄 서적 문서</span>
</label>
<label class="flex items-center">
<input type="radio"
x-model="linkForm.target_type"
value="note"
@change="onTargetTypeChange()"
class="mr-2 text-blue-600">
<span class="text-sm text-gray-700">📝 노트북 노트</span>
</label>
</div>
</div>
<!-- 서적/노트북 선택 -->
<div class="mb-6" x-show="linkForm.target_type">
<label class="block text-sm font-semibold text-gray-700 mb-3"
x-text="linkForm.target_type === 'note' ? '노트북 선택' : '서적 선택'"></label>
<select
x-model="linkForm.target_book_id"
@change="loadDocumentsFromBook()"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
>
<option value="">서적을 선택하세요</option>
<option value="" x-text="linkForm.target_type === 'note' ? '노트북을 선택하세요' : '서적을 선택하세요'"></option>
<template x-for="book in availableBooks" :key="book.id">
<option :value="book.id" x-text="book.title"></option>
</template>
@@ -404,8 +444,10 @@
:disabled="!linkForm.target_book_id"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:bg-gray-100 disabled:cursor-not-allowed">
<option value="">
<span x-show="!linkForm.target_book_id">먼저 서적을 선택하세요</span>
<span x-show="linkForm.target_book_id">문서를 선택하세요</span>
<span x-show="!linkForm.target_book_id"
x-text="linkForm.target_type === 'note' ? '먼저 노트북을 선택하세요' : '먼저 서적을 선택하세요'"></span>
<span x-show="linkForm.target_book_id"
x-text="linkForm.target_type === 'note' ? '노트를 선택하세요' : '문서를 선택하세요'"></span>
</option>
<template x-for="doc in filteredDocuments" :key="doc.id">
<option :value="doc.id" x-text="doc.title"></option>
@@ -501,8 +543,8 @@
<div class="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition-colors cursor-pointer"
@click="scrollToHighlight(note.highlight.id)">
<!-- 선택된 텍스트 -->
<div class="bg-blue-50 rounded-md p-2 mb-3">
<p class="text-sm text-blue-800 font-medium" x-text="note.highlight.selected_text"></p>
<div class="bg-blue-50 rounded-md p-2 mb-3" x-show="note.highlight?.selected_text">
<p class="text-sm text-blue-800 font-medium" x-text="note.highlight?.selected_text || ''"></p>
</div>
<!-- 메모 내용 -->