링크/백링크 기능 수정 및 안정화

- 링크 생성 기능 완전 복구
  - createDocumentLink 함수를 Alpine.js 데이터 객체 내로 이동
  - API 필드명 불일치 수정 (source_text → selected_text 등)
  - 필수 필드 검증 및 상세 에러 로깅 추가

- API 엔드포인트 수정
  - 백엔드와 일치하도록 /documents/{id}/links, /documents/{id}/backlinks 사용
  - 올바른 매개변수 전달 방식 적용

- 링크/백링크 렌더링 안정화
  - forEach 오류 방지를 위한 배열 타입 검증
  - 데이터 없을 시 기존 링크 유지 로직
  - 안전한 초기화 및 에러 처리

- UI 단순화
  - 같은 서적/다른 서적 구분 제거
  - 서적 선택 → 문서 선택 단순한 2단계 프로세스
  - 백링크는 자동 생성되도록 수정

- 디버깅 로그 대폭 강화
  - API 호출, 응답, 렌더링 각 단계별 상세 로깅
  - 데이터 타입 및 개수 추적
This commit is contained in:
Hyungi Ahn
2025-08-28 08:48:52 +09:00
parent 5d4465b15c
commit 844587c86f
5 changed files with 561 additions and 131 deletions

View File

@@ -26,7 +26,7 @@
})();
</script>
<div x-data="documentViewer" x-init="init()">
<div x-data="documentViewer" x-init="init(); $store.documentViewer.init()">
<!-- 헤더 - 투명하고 세련된 3줄 디자인 -->
<header class="bg-white/80 backdrop-blur-md shadow-lg border-b border-white/20 sticky top-0 z-50 w-full">
<div class="w-full px-6 py-4">
@@ -134,67 +134,26 @@
<!-- 중앙: 기능 버튼들 -->
<div class="flex items-center space-x-3">
<!-- 링크 버튼 그룹 -->
<div class="relative">
<button @click="toggleFeatureMenu('link')"
:class="activeFeatureMenu === 'link' ? 'bg-purple-600' : 'bg-purple-500/80 hover:bg-purple-500'"
class="px-4 py-2 text-white rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm">
<i class="fas fa-link text-sm"></i>
<span class="text-sm font-medium">링크</span>
<span x-show="documentLinks.length > 0"
class="bg-white text-purple-600 text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold"
x-text="documentLinks.length"></span>
<i class="fas fa-chevron-down text-xs ml-1" :class="activeFeatureMenu === 'link' ? 'rotate-180' : ''"></i>
</button>
<!-- 링크 서브메뉴 -->
<div x-show="activeFeatureMenu === 'link'"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute top-full left-0 mt-2 bg-white/90 backdrop-blur-md rounded-xl shadow-lg border border-white/30 p-2 z-10 min-w-max">
<button @click="showLinksModal = true; activeFeatureMenu = null"
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-purple-100 rounded-lg flex items-center space-x-2 transition-colors">
<i class="fas fa-eye text-purple-500"></i>
<span>링크 보기</span>
</button>
<button @click="console.log('링크 만들기 클릭됨'); activateLinkMode(); activeFeatureMenu = null"
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-purple-100 rounded-lg flex items-center space-x-2 transition-colors">
<i class="fas fa-plus text-purple-500"></i>
<span>링크 만들기</span>
</button>
</div>
</div>
<!-- 링크 버튼 - 드래그 후 클릭으로 링크 생성 -->
<button @click="activateLinkMode()"
:class="activeMode === 'link' ? 'bg-purple-600' : 'bg-purple-500/80 hover:bg-purple-500'"
class="px-4 py-2 text-white rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm"
title="텍스트를 드래그한 후 이 버튼을 클릭하여 링크를 생성하세요">
<i class="fas fa-link text-sm"></i>
<span class="text-sm font-medium">링크</span>
<span x-show="documentLinks.length > 0"
class="bg-white text-purple-600 text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold"
x-text="documentLinks.length"></span>
</button>
<!-- 백링크 버튼 그룹 -->
<div class="relative">
<button @click="toggleFeatureMenu('backlink')"
:class="activeFeatureMenu === 'backlink' ? 'bg-orange-600' : 'bg-orange-500/80 hover:bg-orange-500'"
class="px-4 py-2 text-white rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm">
<i class="fas fa-arrow-left text-sm"></i>
<span class="text-sm font-medium">백링크</span>
<span x-show="backlinks.length > 0"
class="bg-white text-orange-600 text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold"
x-text="backlinks.length"></span>
<i class="fas fa-chevron-down text-xs ml-1" :class="activeFeatureMenu === 'backlink' ? 'rotate-180' : ''"></i>
</button>
<!-- 백링크 서브메뉴 -->
<div x-show="activeFeatureMenu === 'backlink'"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute top-full left-0 mt-2 bg-white/90 backdrop-blur-md rounded-xl shadow-lg border border-white/30 p-2 z-10 min-w-max">
<button @click="showBacklinksModal = true; loadBacklinks(); activeFeatureMenu = null"
class="w-full px-3 py-2 text-sm text-gray-700 hover:bg-orange-100 rounded-lg flex items-center space-x-2 transition-colors">
<i class="fas fa-eye text-orange-500"></i>
<span>백링크 보기</span>
</button>
</div>
<!-- 백링크 표시 (읽기 전용) -->
<div class="px-4 py-2 bg-orange-500/80 text-white rounded-xl flex items-center space-x-2 shadow-sm"
title="이 문서를 참조하는 다른 문서들">
<i class="fas fa-arrow-left text-sm"></i>
<span class="text-sm font-medium">백링크</span>
<span x-show="backlinks.length > 0"
class="bg-white text-orange-600 text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold"
x-text="backlinks.length"></span>
</div>
<!-- 메모 버튼 그룹 -->
@@ -268,14 +227,14 @@
<!-- 오른쪽: 액션 버튼들 -->
<div class="flex items-center space-x-3">
<button @click="downloadOriginalFile()"
<button @click="$store.documentViewer.downloadOriginalFile()"
class="bg-red-500/80 hover:bg-red-500 text-white px-4 py-2 rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm"
title="원본 PDF 다운로드">
<i class="fas fa-file-pdf text-sm"></i>
<span class="text-sm font-medium">PDF</span>
</button>
<button @click="toggleLanguage()"
<button @click="$store.documentViewer.toggleLanguage()"
class="bg-blue-500/80 hover:bg-blue-500 text-white px-4 py-2 rounded-xl transition-all duration-200 flex items-center space-x-2 shadow-sm"
id="language-toggle-btn">
<i class="fas fa-globe text-sm"></i>
@@ -417,37 +376,14 @@
<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>
<div class="grid grid-cols-2 gap-3">
<button @click="linkForm.book_scope = 'same'; resetTargetSelection()"
:class="linkForm.book_scope === 'same' ? 'bg-green-100 border-green-500 text-green-700' : 'bg-gray-50 border-gray-300 text-gray-600'"
class="p-4 border-2 rounded-lg transition-all duration-200 text-left">
<div class="flex items-center space-x-2 mb-2">
<i class="fas fa-book"></i>
<span class="font-semibold">같은 서적</span>
</div>
<p class="text-xs">현재 서적 내 문서</p>
</button>
<button @click="linkForm.book_scope = 'other'; resetTargetSelection()"
:class="linkForm.book_scope === 'other' ? 'bg-blue-100 border-blue-500 text-blue-700' : 'bg-gray-50 border-gray-300 text-gray-600'"
class="p-4 border-2 rounded-lg transition-all duration-200 text-left">
<div class="flex items-center space-x-2 mb-2">
<i class="fas fa-books"></i>
<span class="font-semibold">다른 서적</span>
</div>
<p class="text-xs">다른 서적의 문서</p>
</button>
</div>
</div>
<!-- 대상 서적 선택 (다른 서적인 경우만) -->
<div x-show="linkForm.book_scope === 'other'" class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">대상 서적</label>
<select x-model="linkForm.target_book_id"
@change="loadDocumentsFromBook()"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<label class="block text-sm font-semibold text-gray-700 mb-3">서적 선택</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>
<template x-for="book in availableBooks" :key="book.id">
<option :value="book.id" x-text="book.title"></option>
@@ -455,21 +391,21 @@
</select>
</div>
<!-- 대상 문서 선택 -->
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">
대상 문서
<span x-show="linkForm.book_scope === 'same'" class="text-green-600 text-xs">(현재 서적)</span>
<span x-show="linkForm.book_scope === 'other' && linkForm.target_book_id" class="text-blue-600 text-xs" x-text="`(${getSelectedBookTitle()})`"></span>
<span x-show="linkForm.target_book_id" class="text-blue-600 text-xs" x-text="`(${getSelectedBookTitle()})`"></span>
</label>
<select x-model="linkForm.target_document_id"
@change="onTargetDocumentChange()"
:disabled="linkForm.book_scope === 'other' && !linkForm.target_book_id"
: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.book_scope === 'same'">문서를 선택하세요</span>
<span x-show="linkForm.book_scope === 'other' && !linkForm.target_book_id">먼저 서적을 선택하세요</span>
<span x-show="linkForm.book_scope === 'other' && linkForm.target_book_id">문서를 선택하세요</span>
<span x-show="!linkForm.target_book_id">먼저 서적을 선택하세요</span>
<span x-show="linkForm.target_book_id">문서를 선택하세요</span>
</option>
<template x-for="doc in filteredDocuments" :key="doc.id">
<option :value="doc.id" x-text="doc.title"></option>
@@ -482,7 +418,7 @@
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">대상 텍스트</label>
<div class="space-y-3">
<button @click="openTargetDocumentSelector()"
<button @click="selectTextFromDocument()"
:disabled="!linkForm.target_document_id"
:class="linkForm.target_document_id ? 'bg-blue-500 hover:bg-blue-600 text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed'"
class="w-full px-4 py-2 rounded-lg transition-colors flex items-center justify-center space-x-2">
@@ -514,7 +450,7 @@
class="px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
취소
</button>
<button @click="saveDocumentLink()"
<button @click="createDocumentLink()"
:disabled="!linkForm?.target_document_id"
:class="linkForm?.target_document_id ? 'bg-purple-500 hover:bg-purple-600' : 'bg-gray-300 cursor-not-allowed'"
class="px-4 py-2 text-white rounded-lg transition-colors">