diff --git a/frontend/static/js/api.js b/frontend/static/js/api.js
index 522c85a..9a1efa3 100644
--- a/frontend/static/js/api.js
+++ b/frontend/static/js/api.js
@@ -259,6 +259,73 @@ class API {
async changePassword(passwordData) {
return await this.put('/auth/change-password', passwordData);
}
+
+ // === 하이라이트 관련 API ===
+ async getDocumentHighlights(documentId) {
+ return await this.get(`/highlights/document/${documentId}`);
+ }
+
+ async createHighlight(highlightData) {
+ return await this.post('/highlights/', highlightData);
+ }
+
+ async updateHighlight(highlightId, highlightData) {
+ return await this.put(`/highlights/${highlightId}`, highlightData);
+ }
+
+ async deleteHighlight(highlightId) {
+ return await this.delete(`/highlights/${highlightId}`);
+ }
+
+ // === 메모 관련 API ===
+ async getDocumentNotes(documentId) {
+ return await this.get(`/notes/document/${documentId}`);
+ }
+
+ async createNote(noteData) {
+ return await this.post('/notes/', noteData);
+ }
+
+ async updateNote(noteId, noteData) {
+ return await this.put(`/notes/${noteId}`, noteData);
+ }
+
+ async deleteNote(noteId) {
+ return await this.delete(`/notes/${noteId}`);
+ }
+
+ async getNotesByHighlight(highlightId) {
+ return await this.get(`/notes/highlight/${highlightId}`);
+ }
+
+ // === 책갈피 관련 API ===
+ async getDocumentBookmarks(documentId) {
+ return await this.get(`/bookmarks/document/${documentId}`);
+ }
+
+ async createBookmark(bookmarkData) {
+ return await this.post('/bookmarks/', bookmarkData);
+ }
+
+ async updateBookmark(bookmarkId, bookmarkData) {
+ return await this.put(`/bookmarks/${bookmarkId}`, bookmarkData);
+ }
+
+ async deleteBookmark(bookmarkId) {
+ return await this.delete(`/bookmarks/${bookmarkId}`);
+ }
+
+ // === 검색 관련 API ===
+ async searchDocuments(query, filters = {}) {
+ const params = new URLSearchParams({ q: query, ...filters });
+ return await this.get(`/search/documents?${params}`);
+ }
+
+ async searchNotes(query, documentId = null) {
+ const params = new URLSearchParams({ q: query });
+ if (documentId) params.append('document_id', documentId);
+ return await this.get(`/search/notes?${params}`);
+ }
}
// 전역 API 인스턴스
diff --git a/frontend/static/js/main.js b/frontend/static/js/main.js
index 778054b..be611df 100644
--- a/frontend/static/js/main.js
+++ b/frontend/static/js/main.js
@@ -185,6 +185,11 @@ window.documentApp = () => ({
}
},
+ // 문서 뷰어 열기
+ openDocument(documentId) {
+ window.location.href = `/viewer.html?id=${documentId}`;
+ },
+
// 날짜 포맷팅
formatDate(dateString) {
const date = new Date(dateString);
diff --git a/frontend/static/js/viewer.js b/frontend/static/js/viewer.js
index 6e048a1..2460312 100644
--- a/frontend/static/js/viewer.js
+++ b/frontend/static/js/viewer.js
@@ -48,6 +48,9 @@ window.documentViewer = () => ({
// 초기화
async init() {
+ // 전역 인스턴스 설정 (말풍선에서 함수 호출용)
+ window.documentViewerInstance = this;
+
// URL에서 문서 ID 추출
const urlParams = new URLSearchParams(window.location.search);
this.documentId = urlParams.get('id');
@@ -78,31 +81,19 @@ window.documentViewer = () => ({
this.filterNotes();
},
- // 문서 로드 (목업 + 실제 HTML)
+ // 문서 로드 (실제 API 연동)
async loadDocument() {
- // 목업 문서 정보
- const mockDocuments = {
- 'test-doc-1': {
- id: 'test-doc-1',
- title: 'Document Server 테스트 문서',
- description: '하이라이트와 메모 기능을 테스트하기 위한 샘플 문서입니다.',
- uploader_name: '관리자'
- },
- 'test': {
- id: 'test',
- title: 'Document Server 테스트 문서',
- description: '하이라이트와 메모 기능을 테스트하기 위한 샘플 문서입니다.',
- uploader_name: '관리자'
- }
- };
-
- this.document = mockDocuments[this.documentId] || mockDocuments['test'];
-
- // HTML 내용 로드 (실제 파일)
try {
- const response = await fetch('/uploads/documents/test-document.html');
+ // 백엔드에서 문서 정보 가져오기
+ this.document = await api.getDocument(this.documentId);
+
+ // HTML 파일 경로 구성 (백엔드 서버를 통해 접근)
+ const htmlPath = this.document.html_path;
+ const fileName = htmlPath.split('/').pop();
+ const response = await fetch(`http://localhost:24102/uploads/documents/${fileName}`);
+
if (!response.ok) {
- throw new Error('문서를 불러올 수 없습니다');
+ throw new Error('문서 파일을 불러올 수 없습니다');
}
const htmlContent = await response.text();
@@ -110,8 +101,23 @@ window.documentViewer = () => ({
// 페이지 제목 업데이트
document.title = `${this.document.title} - Document Server`;
+
+ // 문서 내 스크립트 오류 방지를 위한 전역 함수들 정의
+ this.setupDocumentScriptHandlers();
+
} catch (error) {
- // 파일이 없으면 기본 내용 표시
+ console.error('Document load error:', error);
+
+ // 백엔드 연결 실패시 목업 데이터로 폴백
+ console.warn('Using fallback mock data');
+ this.document = {
+ id: this.documentId,
+ title: 'Document Server 테스트 문서',
+ description: '하이라이트와 메모 기능을 테스트하기 위한 샘플 문서입니다.',
+ uploader_name: '관리자'
+ };
+
+ // 기본 HTML 내용 표시
document.getElementById('document-content').innerHTML = `
테스트 문서
이 문서는 Document Server의 하이라이트 및 메모 기능을 테스트하기 위한 샘플입니다.
@@ -123,28 +129,115 @@ window.documentViewer = () => ({
메모 검색 및 관리
책갈피 기능
+
테스트 단락
+
이것은 하이라이트 테스트를 위한 긴 단락입니다. 이 텍스트를 선택하여 하이라이트를 만들어보세요.
+ 하이라이트를 만든 후에는 메모를 추가할 수 있습니다. 메모는 나중에 검색하고 편집할 수 있습니다.
+
또 다른 단락입니다. 여러 개의 하이라이트를 만들어서 메모 기능을 테스트해보세요.
+ 각 하이라이트는 고유한 색상을 가질 수 있으며, 연결된 메모를 통해 중요한 정보를 기록할 수 있습니다.
`;
+
+ // 폴백 모드에서도 스크립트 핸들러 설정
+ this.setupDocumentScriptHandlers();
+
+ // 디버깅을 위한 전역 함수 노출
+ window.testHighlight = () => {
+ console.log('Test highlight function called');
+ const selection = window.getSelection();
+ console.log('Current selection:', selection.toString());
+ this.handleTextSelection();
+ };
}
},
+ // 문서 내 스크립트 핸들러 설정
+ setupDocumentScriptHandlers() {
+ // 업로드된 HTML 문서에서 사용할 수 있는 전역 함수들 정의
+
+ // 언어 토글 함수 (많은 문서에서 사용)
+ window.toggleLanguage = function() {
+ const koreanContent = document.getElementById('korean-content');
+ const englishContent = document.getElementById('english-content');
+
+ if (koreanContent && englishContent) {
+ // ID 기반 토글 (압력용기 매뉴얼 등)
+ if (koreanContent.style.display === 'none') {
+ koreanContent.style.display = 'block';
+ englishContent.style.display = 'none';
+ } else {
+ koreanContent.style.display = 'none';
+ englishContent.style.display = 'block';
+ }
+ } else {
+ // 클래스 기반 토글 (다른 문서들)
+ const koreanElements = document.querySelectorAll('.korean, .ko');
+ const englishElements = document.querySelectorAll('.english, .en');
+
+ koreanElements.forEach(el => {
+ el.style.display = el.style.display === 'none' ? 'block' : 'none';
+ });
+
+ englishElements.forEach(el => {
+ el.style.display = el.style.display === 'none' ? 'block' : 'none';
+ });
+ }
+
+ // 토글 버튼 텍스트 업데이트
+ const toggleButton = document.querySelector('.language-toggle');
+ if (toggleButton && koreanContent) {
+ const isKoreanVisible = koreanContent.style.display !== 'none';
+ toggleButton.textContent = isKoreanVisible ? '🌐 English' : '🌐 한국어';
+ }
+ };
+
+ // 기타 공통 함수들 (필요시 추가)
+ window.showSection = function(sectionId) {
+ const section = document.getElementById(sectionId);
+ if (section) {
+ section.scrollIntoView({ behavior: 'smooth' });
+ }
+ };
+
+ // 인쇄 함수
+ window.printDocument = function() {
+ window.print();
+ };
+
+ // 문서 내 링크 클릭 시 새 창에서 열기 방지
+ const links = document.querySelectorAll('#document-content a[href^="http"]');
+ links.forEach(link => {
+ link.addEventListener('click', (e) => {
+ e.preventDefault();
+ if (confirm('외부 링크로 이동하시겠습니까?\n' + link.href)) {
+ window.open(link.href, '_blank');
+ }
+ });
+ });
+ },
+
// 문서 관련 데이터 로드
async loadDocumentData() {
try {
+ console.log('Loading document data for:', this.documentId);
const [highlights, notes, bookmarks] = await Promise.all([
- api.getDocumentHighlights(this.documentId),
- api.getDocumentNotes(this.documentId),
- api.getDocumentBookmarks(this.documentId)
+ api.getDocumentHighlights(this.documentId).catch(() => []),
+ api.getDocumentNotes(this.documentId).catch(() => []),
+ api.getDocumentBookmarks(this.documentId).catch(() => [])
]);
- this.highlights = highlights;
- this.notes = notes;
- this.bookmarks = bookmarks;
+ this.highlights = highlights || [];
+ this.notes = notes || [];
+ this.bookmarks = bookmarks || [];
+
+ console.log('Loaded data:', { highlights: this.highlights.length, notes: this.notes.length, bookmarks: this.bookmarks.length });
// 하이라이트 렌더링
this.renderHighlights();
} catch (error) {
- console.error('Failed to load document data:', error);
+ console.warn('Some document data failed to load, continuing with empty data:', error);
+ this.highlights = [];
+ this.notes = [];
+ this.bookmarks = [];
}
},
@@ -203,10 +296,10 @@ window.documentViewer = () => ({
highlightEl.textContent = highlightText;
highlightEl.dataset.highlightId = highlight.id;
- // 클릭 이벤트 추가
+ // 클릭 이벤트 추가 - 말풍선 표시
highlightEl.addEventListener('click', (e) => {
e.stopPropagation();
- this.selectHighlight(highlight.id);
+ this.showHighlightTooltip(highlight, e.target);
});
// 노드 교체
@@ -227,22 +320,27 @@ window.documentViewer = () => ({
// 텍스트 선택 처리
handleTextSelection() {
+ console.log('handleTextSelection called');
const selection = window.getSelection();
if (selection.rangeCount === 0 || selection.isCollapsed) {
+ console.log('No selection or collapsed');
return;
}
const range = selection.getRangeAt(0);
const selectedText = selection.toString().trim();
+ console.log('Selected text:', selectedText);
if (selectedText.length < 2) {
+ console.log('Text too short');
return;
}
// 문서 컨텐츠 내부의 선택인지 확인
const content = document.getElementById('document-content');
if (!content.contains(range.commonAncestorContainer)) {
+ console.log('Selection not in document content');
return;
}
@@ -250,6 +348,7 @@ window.documentViewer = () => ({
this.selectedText = selectedText;
this.selectedRange = range.cloneRange();
+ console.log('Showing highlight button');
// 컨텍스트 메뉴 표시 (간단한 버튼)
this.showHighlightButton(selection);
},
@@ -266,10 +365,12 @@ window.documentViewer = () => ({
const rect = range.getBoundingClientRect();
const button = document.createElement('button');
- button.className = 'highlight-button fixed z-50 bg-blue-600 text-white px-3 py-1 rounded shadow-lg text-sm';
+ button.className = 'highlight-button fixed z-50 bg-blue-600 text-white px-4 py-2 rounded shadow-lg text-sm font-medium border-2 border-blue-700';
button.style.left = `${rect.left + window.scrollX}px`;
- button.style.top = `${rect.bottom + window.scrollY + 5}px`;
- button.innerHTML = '
하이라이트';
+ button.style.top = `${rect.bottom + window.scrollY + 10}px`;
+ button.innerHTML = '🖍️ 하이라이트';
+
+ console.log('Highlight button created at:', button.style.left, button.style.top);
button.addEventListener('click', () => {
this.createHighlight();
@@ -286,16 +387,44 @@ window.documentViewer = () => ({
}, 3000);
},
+ // 색상 버튼으로 하이라이트 생성
+ createHighlightWithColor(color) {
+ console.log('createHighlightWithColor called with color:', color);
+
+ // 현재 선택된 텍스트가 있는지 확인
+ const selection = window.getSelection();
+ if (selection.rangeCount === 0 || selection.isCollapsed) {
+ alert('먼저 하이라이트할 텍스트를 선택해주세요.');
+ return;
+ }
+
+ // 색상 설정 후 하이라이트 생성
+ this.selectedHighlightColor = color;
+ this.handleTextSelection(); // 텍스트 선택 처리
+
+ // 바로 하이라이트 생성 (버튼 클릭 없이)
+ setTimeout(() => {
+ this.createHighlight();
+ }, 100);
+ },
+
// 하이라이트 생성
async createHighlight() {
+ console.log('createHighlight called');
+ console.log('selectedText:', this.selectedText);
+ console.log('selectedRange:', this.selectedRange);
+
if (!this.selectedText || !this.selectedRange) {
+ console.log('No selected text or range');
return;
}
try {
+ console.log('Starting highlight creation...');
// 텍스트 오프셋 계산
const content = document.getElementById('document-content');
const { startOffset, endOffset } = this.calculateTextOffsets(this.selectedRange, content);
+ console.log('Text offsets:', startOffset, endOffset);
const highlightData = {
document_id: this.documentId,
@@ -669,5 +798,232 @@ window.documentViewer = () => ({
hour: '2-digit',
minute: '2-digit'
});
+ },
+
+ // 하이라이트 말풍선 표시
+ showHighlightTooltip(highlight, element) {
+ // 기존 말풍선 제거
+ this.hideTooltip();
+
+ const tooltip = document.createElement('div');
+ tooltip.id = 'highlight-tooltip';
+ tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50 max-w-sm';
+ tooltip.style.minWidth = '300px';
+
+ // 하이라이트 정보와 메모 표시
+ const highlightNotes = this.notes.filter(note => note.highlight_id === highlight.id);
+
+ tooltip.innerHTML = `
+
+
선택된 텍스트
+
+ "${highlight.selected_text}"
+
+
+
+
+
+ 메모 (${highlightNotes.length})
+
+
+
+
+ ${highlightNotes.length > 0 ?
+ highlightNotes.map(note => `
+
+
${note.content}
+
+ ${this.formatShortDate(note.created_at)} · Administrator
+
+
+ `).join('') :
+ '
메모가 없습니다
'
+ }
+
+
+
+
+
+
+
+ `;
+
+ // 위치 계산
+ const rect = element.getBoundingClientRect();
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
+
+ document.body.appendChild(tooltip);
+
+ // 말풍선 위치 조정
+ const tooltipRect = tooltip.getBoundingClientRect();
+ let top = rect.bottom + scrollTop + 5;
+ let left = rect.left + scrollLeft;
+
+ // 화면 경계 체크
+ if (left + tooltipRect.width > window.innerWidth) {
+ left = window.innerWidth - tooltipRect.width - 10;
+ }
+ if (top + tooltipRect.height > window.innerHeight + scrollTop) {
+ top = rect.top + scrollTop - tooltipRect.height - 5;
+ }
+
+ tooltip.style.top = top + 'px';
+ tooltip.style.left = left + 'px';
+
+ // 외부 클릭 시 닫기
+ setTimeout(() => {
+ document.addEventListener('click', this.handleTooltipOutsideClick.bind(this));
+ }, 100);
+ },
+
+ // 말풍선 숨기기
+ hideTooltip() {
+ const tooltip = document.getElementById('highlight-tooltip');
+ if (tooltip) {
+ tooltip.remove();
+ }
+ document.removeEventListener('click', this.handleTooltipOutsideClick.bind(this));
+ },
+
+ // 말풍선 외부 클릭 처리
+ handleTooltipOutsideClick(e) {
+ const tooltip = document.getElementById('highlight-tooltip');
+ if (tooltip && !tooltip.contains(e.target) && !e.target.classList.contains('highlight')) {
+ this.hideTooltip();
+ }
+ },
+
+ // 짧은 날짜 형식
+ formatShortDate(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 {
+ return date.toLocaleDateString('ko-KR', {
+ year: '2-digit',
+ month: '2-digit',
+ day: '2-digit'
+ });
+ }
+ },
+
+ // 메모 추가 폼 표시
+ showAddNoteForm(highlightId) {
+ const tooltip = document.getElementById('highlight-tooltip');
+ if (!tooltip) return;
+
+ const notesList = tooltip.querySelector('#notes-list');
+ notesList.innerHTML = `
+
+
+
+
+
+
+
+ `;
+
+ // 텍스트 영역에 포커스
+ setTimeout(() => {
+ document.getElementById('new-note-content').focus();
+ }, 100);
+ },
+
+ // 메모 추가 취소
+ cancelAddNote(highlightId) {
+ // 말풍선 다시 표시
+ const highlight = this.highlights.find(h => h.id === highlightId);
+ if (highlight) {
+ const element = document.querySelector(`[data-highlight-id="${highlightId}"]`);
+ if (element) {
+ this.showHighlightTooltip(highlight, element);
+ }
+ }
+ },
+
+ // 새 메모 저장
+ async saveNewNote(highlightId) {
+ const content = document.getElementById('new-note-content').value.trim();
+ if (!content) {
+ alert('메모 내용을 입력해주세요');
+ return;
+ }
+
+ try {
+ const noteData = {
+ highlight_id: highlightId,
+ content: content,
+ is_private: false,
+ tags: []
+ };
+
+ const newNote = await api.createNote(noteData);
+
+ // 로컬 데이터 업데이트
+ this.notes.push(newNote);
+
+ // 말풍선 새로고침
+ const highlight = this.highlights.find(h => h.id === highlightId);
+ if (highlight) {
+ const element = document.querySelector(`[data-highlight-id="${highlightId}"]`);
+ if (element) {
+ this.showHighlightTooltip(highlight, element);
+ }
+ }
+
+ } catch (error) {
+ console.error('Failed to save note:', error);
+ alert('메모 저장에 실패했습니다');
+ }
+ },
+
+ // 하이라이트 삭제
+ async deleteHighlight(highlightId) {
+ if (!confirm('이 하이라이트를 삭제하시겠습니까? 연결된 메모도 함께 삭제됩니다.')) {
+ return;
+ }
+
+ try {
+ await api.deleteHighlight(highlightId);
+
+ // 로컬 데이터에서 제거
+ this.highlights = this.highlights.filter(h => h.id !== highlightId);
+ this.notes = this.notes.filter(n => n.highlight_id !== highlightId);
+
+ // UI 업데이트
+ this.hideTooltip();
+ this.renderHighlights();
+
+ } catch (error) {
+ console.error('Failed to delete highlight:', error);
+ alert('하이라이트 삭제에 실패했습니다');
+ }
}
});
diff --git a/frontend/viewer.html b/frontend/viewer.html
index 6431984..66117bc 100644
--- a/frontend/viewer.html
+++ b/frontend/viewer.html
@@ -33,18 +33,22 @@
-
-
+
-
+
-
+
+ class="w-8 h-8 bg-blue-300 rounded border-2 border-white"
+ title="파란색 하이라이트">