feat: 플로팅 메모창 구현 및 문서 삭제 기능 안정화
✨ 새로운 기능: - 플로팅 메모창: 드래그 가능한 독립적인 메모/하이라이트 창 - 크기 조절: 마우스로 창 크기 자유롭게 조정 - 위치 이동: 헤더 드래그로 원하는 위치에 배치 - 메모 검색: 실시간 메모 내용 검색 및 필터링 - 하이라이트 이동: 메모창에서 문서 내 하이라이트 위치로 스크롤 🛠️ 개선사항: - 문서 삭제 안정화: 외래키 제약조건 해결 (Note->Highlight->Document 순서) - 레이아웃 개선: 문서 뷰어가 전체 화면 사용 가능 - 사용자 경험: 사이드바 대신 플로팅 윈도우로 더 유연한 UI - 캐시 버스팅: JavaScript 파일 버전 관리 개선 🐛 버그 수정: - 중복 스크립트 로드 문제 해결 - 하이라이트 메모 업데이트 API 오류 수정 - 문서 삭제 시 500 에러 해결
This commit is contained in:
@@ -506,25 +506,55 @@ async def delete_document(
|
|||||||
if document.thumbnail_path and os.path.exists(document.thumbnail_path):
|
if document.thumbnail_path and os.path.exists(document.thumbnail_path):
|
||||||
os.remove(document.thumbnail_path)
|
os.remove(document.thumbnail_path)
|
||||||
|
|
||||||
# 관련 데이터 먼저 삭제 (외래키 제약 조건 해결)
|
# 관련 데이터 안전하게 삭제 (외래키 제약 조건 해결)
|
||||||
from ...models.highlight import Highlight
|
from ...models.highlight import Highlight
|
||||||
from ...models.note import Note
|
from ...models.note import Note
|
||||||
from ...models.bookmark import Bookmark
|
from ...models.bookmark import Bookmark
|
||||||
|
|
||||||
# 메모 먼저 삭제 (하이라이트를 참조하므로)
|
try:
|
||||||
await db.execute(delete(Note).where(Note.document_id == document_id))
|
print(f"DEBUG: Starting deletion of document {document_id}")
|
||||||
|
|
||||||
# 북마크 삭제
|
# 1. 먼저 해당 문서의 모든 하이라이트 ID 조회
|
||||||
await db.execute(delete(Bookmark).where(Bookmark.document_id == document_id))
|
highlight_ids_result = await db.execute(select(Highlight.id).where(Highlight.document_id == document_id))
|
||||||
|
highlight_ids = [row[0] for row in highlight_ids_result.fetchall()]
|
||||||
|
print(f"DEBUG: Found {len(highlight_ids)} highlights to delete")
|
||||||
|
|
||||||
# 마지막으로 하이라이트 삭제
|
# 2. 하이라이트에 연결된 모든 메모 삭제
|
||||||
await db.execute(delete(Highlight).where(Highlight.document_id == document_id))
|
total_notes_deleted = 0
|
||||||
|
for highlight_id in highlight_ids:
|
||||||
|
note_result = await db.execute(delete(Note).where(Note.highlight_id == highlight_id))
|
||||||
|
total_notes_deleted += note_result.rowcount
|
||||||
|
print(f"DEBUG: Deleted {total_notes_deleted} notes by highlight_id")
|
||||||
|
|
||||||
# 문서-태그 관계 삭제 (Document.tags 관계를 통해 자동 처리됨)
|
# 3. document_id로 직접 연결된 메모도 삭제 (혹시 있다면)
|
||||||
|
direct_note_result = await db.execute(delete(Note).where(Note.document_id == document_id))
|
||||||
|
print(f"DEBUG: Deleted {direct_note_result.rowcount} notes by document_id")
|
||||||
|
|
||||||
# 마지막으로 문서 삭제
|
# 4. 북마크 삭제
|
||||||
await db.execute(delete(Document).where(Document.id == document_id))
|
bookmark_result = await db.execute(delete(Bookmark).where(Bookmark.document_id == document_id))
|
||||||
|
print(f"DEBUG: Deleted {bookmark_result.rowcount} bookmarks")
|
||||||
|
|
||||||
|
# 5. 하이라이트 삭제 (이제 메모가 모두 삭제되었으므로 안전)
|
||||||
|
highlight_result = await db.execute(delete(Highlight).where(Highlight.document_id == document_id))
|
||||||
|
print(f"DEBUG: Deleted {highlight_result.rowcount} highlights")
|
||||||
|
|
||||||
|
# 6. 문서-태그 관계는 SQLAlchemy가 자동으로 처리
|
||||||
|
|
||||||
|
# 7. 마지막으로 문서 삭제
|
||||||
|
doc_result = await db.execute(delete(Document).where(Document.id == document_id))
|
||||||
|
print(f"DEBUG: Deleted {doc_result.rowcount} documents")
|
||||||
|
|
||||||
|
# 8. 커밋
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
print(f"DEBUG: Successfully deleted document {document_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to delete document {document_id}: {e}")
|
||||||
|
await db.rollback()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to delete document: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
return {"message": "Document deleted successfully"}
|
return {"message": "Document deleted successfully"}
|
||||||
|
|
||||||
|
|||||||
@@ -381,9 +381,30 @@ async def delete_highlight(
|
|||||||
detail="Not enough permissions"
|
detail="Not enough permissions"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 하이라이트 삭제 (CASCADE로 메모도 함께 삭제됨)
|
# 안전한 하이라이트 삭제 (연결된 메모 먼저 삭제)
|
||||||
await db.execute(delete(Highlight).where(Highlight.id == highlight_id))
|
try:
|
||||||
|
print(f"DEBUG: Starting deletion of highlight {highlight_id}")
|
||||||
|
|
||||||
|
# 1. 먼저 연결된 메모 삭제
|
||||||
|
from ...models.note import Note
|
||||||
|
note_result = await db.execute(delete(Note).where(Note.highlight_id == highlight_id))
|
||||||
|
print(f"DEBUG: Deleted {note_result.rowcount} notes for highlight {highlight_id}")
|
||||||
|
|
||||||
|
# 2. 하이라이트 삭제
|
||||||
|
highlight_result = await db.execute(delete(Highlight).where(Highlight.id == highlight_id))
|
||||||
|
print(f"DEBUG: Deleted {highlight_result.rowcount} highlights")
|
||||||
|
|
||||||
|
# 3. 커밋
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
print(f"DEBUG: Successfully deleted highlight {highlight_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to delete highlight {highlight_id}: {e}")
|
||||||
|
await db.rollback()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to delete highlight: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
return {"message": "Highlight deleted successfully"}
|
return {"message": "Highlight deleted successfully"}
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
|
|
||||||
<!-- 메인 컨테이너 -->
|
<!-- 메인 컨테이너 -->
|
||||||
<div class="flex h-screen pt-16">
|
<div class="flex h-screen pt-16">
|
||||||
<!-- 사이드바 -->
|
<!-- 왼쪽 사이드바 (항상 표시) -->
|
||||||
<aside class="w-80 bg-white shadow-lg border-r flex flex-col">
|
<aside class="w-80 bg-white shadow-lg border-r flex flex-col">
|
||||||
<!-- 사이드바 헤더 -->
|
<!-- 사이드바 헤더 -->
|
||||||
<div class="p-4 border-b bg-gray-50">
|
<div class="p-4 border-b bg-gray-50">
|
||||||
@@ -378,6 +378,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-show="selectedDocument" class="flex space-x-2">
|
<div x-show="selectedDocument" class="flex space-x-2">
|
||||||
|
<!-- 플로팅 메모창 토글 -->
|
||||||
|
<button
|
||||||
|
@click="showFloatingMemo = !showFloatingMemo"
|
||||||
|
class="px-3 py-2 rounded transition-colors"
|
||||||
|
:class="showFloatingMemo ? 'bg-blue-500 text-white' : 'text-blue-600 border border-blue-600 hover:bg-blue-50'"
|
||||||
|
title="메모 & 하이라이트 창"
|
||||||
|
>
|
||||||
|
<i class="fas fa-window-restore mr-1"></i>
|
||||||
|
메모창 (<span x-text="notes.length + highlights.length"></span>)
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="toggleHighlightMode()"
|
@click="toggleHighlightMode()"
|
||||||
class="px-3 py-2 rounded transition-colors"
|
class="px-3 py-2 rounded transition-colors"
|
||||||
@@ -441,9 +452,154 @@
|
|||||||
<button onclick="window.hierarchyInstance.createHighlight('red')" class="w-8 h-8 bg-red-300 rounded hover:bg-red-400" title="빨강"></button>
|
<button onclick="window.hierarchyInstance.createHighlight('red')" class="w-8 h-8 bg-red-300 rounded hover:bg-red-400" title="빨강"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 플로팅 메모창 -->
|
||||||
|
<div
|
||||||
|
x-show="showFloatingMemo && selectedDocument"
|
||||||
|
class="fixed bg-white rounded-lg shadow-2xl border z-40 flex flex-col"
|
||||||
|
:style="`left: ${floatingMemoPosition.x}px; top: ${floatingMemoPosition.y}px; width: ${floatingMemoSize.width}px; height: ${floatingMemoSize.height}px;`"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<!-- 플로팅 창 헤더 (드래그 가능) -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-t-lg cursor-move"
|
||||||
|
@mousedown="startDragging($event)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i class="fas fa-window-restore"></i>
|
||||||
|
<h3 class="font-semibold">📝 메모 & 하이라이트</h3>
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<span class="text-xs bg-white bg-opacity-20 px-2 py-1 rounded-full" x-text="`${notes.length}개 메모`"></span>
|
||||||
|
<span class="text-xs bg-white bg-opacity-20 px-2 py-1 rounded-full" x-text="`${highlights.length}개 하이라이트`"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<!-- 크기 조절 버튼들 -->
|
||||||
|
<button
|
||||||
|
@click="toggleFloatingMemoSize()"
|
||||||
|
class="text-white hover:bg-white hover:bg-opacity-20 p-1 rounded"
|
||||||
|
title="크기 조절"
|
||||||
|
>
|
||||||
|
<i class="fas fa-expand-arrows-alt text-sm"></i>
|
||||||
|
</button>
|
||||||
|
<!-- 닫기 버튼 -->
|
||||||
|
<button
|
||||||
|
@click="showFloatingMemo = false"
|
||||||
|
class="text-white hover:bg-white hover:bg-opacity-20 p-1 rounded"
|
||||||
|
title="닫기"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 메모 검색 -->
|
||||||
|
<div class="p-3 border-b bg-gray-50">
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
x-model="memoSearchQuery"
|
||||||
|
@input="filterMemos()"
|
||||||
|
placeholder="메모 검색..."
|
||||||
|
class="w-full pl-8 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-search absolute left-2.5 top-2.5 text-gray-400 text-sm"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 메모 목록 -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-3">
|
||||||
|
<div x-show="(filteredNotes.length === 0 && highlights.length === 0)" class="text-center text-gray-500 py-8">
|
||||||
|
<i class="fas fa-sticky-note text-3xl text-gray-300 mb-2"></i>
|
||||||
|
<p class="text-sm">아직 메모나 하이라이트가 없습니다.</p>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">문서에서 텍스트를 선택하여 하이라이트를 만들어보세요.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 하이라이트 섹션 -->
|
||||||
|
<div x-show="highlights.length > 0" class="mb-4">
|
||||||
|
<h4 class="font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<i class="fas fa-highlighter text-yellow-500 mr-2"></i>
|
||||||
|
하이라이트 (<span x-text="highlights.length"></span>)
|
||||||
|
</h4>
|
||||||
|
<template x-for="highlight in highlights" :key="highlight.id">
|
||||||
|
<div class="mb-3 p-3 bg-gray-50 rounded-lg border-l-4" :class="{
|
||||||
|
'border-yellow-400': highlight.highlight_color === 'yellow',
|
||||||
|
'border-blue-400': highlight.highlight_color === 'blue',
|
||||||
|
'border-green-400': highlight.highlight_color === 'green',
|
||||||
|
'border-red-400': highlight.highlight_color === 'red'
|
||||||
|
}">
|
||||||
|
<div class="text-sm text-gray-600 mb-1" x-text="highlight.selected_text"></div>
|
||||||
|
<div x-show="highlight.notes && highlight.notes.content" class="text-xs text-gray-500 mt-2 p-2 bg-white rounded border">
|
||||||
|
<i class="fas fa-comment mr-1"></i>
|
||||||
|
<span x-text="highlight.notes.content"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between mt-2">
|
||||||
|
<span class="text-xs text-gray-400" x-text="new Date(highlight.created_at).toLocaleDateString()"></span>
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<button
|
||||||
|
@click="scrollToHighlight(highlight.id)"
|
||||||
|
class="text-xs text-blue-600 hover:text-blue-800 p-1"
|
||||||
|
title="하이라이트로 이동"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deleteHighlight(highlight.id)"
|
||||||
|
class="text-xs text-red-600 hover:text-red-800 p-1"
|
||||||
|
title="삭제"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 독립 메모 섹션 -->
|
||||||
|
<div x-show="filteredNotes.length > 0">
|
||||||
|
<h4 class="font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<i class="fas fa-sticky-note text-blue-500 mr-2"></i>
|
||||||
|
독립 메모 (<span x-text="filteredNotes.length"></span>)
|
||||||
|
</h4>
|
||||||
|
<template x-for="note in filteredNotes" :key="note.id">
|
||||||
|
<div class="mb-3 p-3 bg-blue-50 rounded-lg border-l-4 border-blue-400">
|
||||||
|
<div class="text-sm text-gray-700" x-text="note.content"></div>
|
||||||
|
<div class="flex items-center justify-between mt-2">
|
||||||
|
<span class="text-xs text-gray-400" x-text="new Date(note.created_at).toLocaleDateString()"></span>
|
||||||
|
<button
|
||||||
|
@click="deleteNote(note.id)"
|
||||||
|
class="text-xs text-red-600 hover:text-red-800 p-1"
|
||||||
|
title="삭제"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 크기 조절 핸들 -->
|
||||||
|
<div
|
||||||
|
class="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize bg-gray-300 hover:bg-gray-400"
|
||||||
|
@mousedown="startResizing($event)"
|
||||||
|
title="크기 조절"
|
||||||
|
>
|
||||||
|
<i class="fas fa-grip-lines text-xs text-gray-600 absolute bottom-0.5 right-0.5"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 로그인 모달 -->
|
<!-- 로그인 모달 -->
|
||||||
<div x-data="authModal()" x-show="showLoginModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div x-data="authModal()" x-show="showLoginModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
|
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
|
||||||
|
|||||||
@@ -458,10 +458,7 @@
|
|||||||
<!-- 인증 모달 컴포넌트 -->
|
<!-- 인증 모달 컴포넌트 -->
|
||||||
<div x-data="authModal" x-ref="authModal"></div>
|
<div x-data="authModal" x-ref="authModal"></div>
|
||||||
|
|
||||||
<!-- 스크립트 -->
|
<!-- 스크립트는 하단에서 로드됩니다 -->
|
||||||
<script src="/static/js/api.js"></script>
|
|
||||||
<script src="/static/js/auth.js"></script>
|
|
||||||
<script src="/static/js/main.js"></script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
@@ -480,8 +477,8 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- JavaScript 파일들 -->
|
<!-- JavaScript 파일들 -->
|
||||||
<script src="/static/js/api.js?v=2025012222"></script>
|
<script src="/static/js/api.js?v=2025012225"></script>
|
||||||
<script src="/static/js/auth.js?v=2025012222"></script>
|
<script src="/static/js/auth.js?v=2025012225"></script>
|
||||||
<script src="/static/js/main.js?v=2025012222"></script>
|
<script src="/static/js/main.js?v=2025012225"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ window.hierarchyApp = function() {
|
|||||||
highlightMenuPosition: { x: 0, y: 0 },
|
highlightMenuPosition: { x: 0, y: 0 },
|
||||||
highlightMode: false, // 하이라이트 모드 토글
|
highlightMode: false, // 하이라이트 모드 토글
|
||||||
|
|
||||||
|
// 메모 사이드바
|
||||||
|
memoSearchQuery: '',
|
||||||
|
filteredNotes: [],
|
||||||
|
|
||||||
|
// 플로팅 메모창
|
||||||
|
showFloatingMemo: false,
|
||||||
|
floatingMemoPosition: { x: 100, y: 100 },
|
||||||
|
floatingMemoSize: { width: 400, height: 500 },
|
||||||
|
isDragging: false,
|
||||||
|
isResizing: false,
|
||||||
|
dragOffset: { x: 0, y: 0 },
|
||||||
|
|
||||||
// 드래그 앤 드롭
|
// 드래그 앤 드롭
|
||||||
draggedDocument: null,
|
draggedDocument: null,
|
||||||
|
|
||||||
@@ -193,6 +205,9 @@ window.hierarchyApp = function() {
|
|||||||
this.highlights = highlights || [];
|
this.highlights = highlights || [];
|
||||||
this.notes = notes || [];
|
this.notes = notes || [];
|
||||||
|
|
||||||
|
// 메모 사이드바용 필터링된 노트 초기화
|
||||||
|
this.filterMemos();
|
||||||
|
|
||||||
console.log(`📝 최종 저장: 하이라이트 ${this.highlights.length}개, 메모 ${this.notes.length}개`);
|
console.log(`📝 최종 저장: 하이라이트 ${this.highlights.length}개, 메모 ${this.notes.length}개`);
|
||||||
|
|
||||||
if (this.highlights.length > 0) {
|
if (this.highlights.length > 0) {
|
||||||
@@ -1108,6 +1123,119 @@ window.authModal = function() {
|
|||||||
resetLoginForm() {
|
resetLoginForm() {
|
||||||
this.loginForm = { email: '', password: '' };
|
this.loginForm = { email: '', password: '' };
|
||||||
this.loginError = null;
|
this.loginError = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 메모 필터링
|
||||||
|
filterMemos() {
|
||||||
|
console.log('🔍 메모 필터링 시작:', {
|
||||||
|
totalNotes: this.notes.length,
|
||||||
|
searchQuery: this.memoSearchQuery,
|
||||||
|
notes: this.notes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.memoSearchQuery.trim()) {
|
||||||
|
this.filteredNotes = this.notes.filter(note => !note.highlight_id);
|
||||||
|
console.log('📝 독립 메모 필터링 결과:', this.filteredNotes.length, this.filteredNotes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.memoSearchQuery.toLowerCase();
|
||||||
|
this.filteredNotes = this.notes
|
||||||
|
.filter(note => !note.highlight_id) // 독립 메모만
|
||||||
|
.filter(note => note.content.toLowerCase().includes(query));
|
||||||
|
console.log('🔍 검색 결과:', this.filteredNotes.length, this.filteredNotes);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 하이라이트로 스크롤
|
||||||
|
scrollToHighlight(highlightId) {
|
||||||
|
const highlightElement = document.querySelector(`[data-highlight-id="${highlightId}"]`);
|
||||||
|
if (highlightElement) {
|
||||||
|
highlightElement.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 잠시 강조 효과
|
||||||
|
highlightElement.style.boxShadow = '0 0 10px rgba(59, 130, 246, 0.5)';
|
||||||
|
setTimeout(() => {
|
||||||
|
highlightElement.style.boxShadow = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 플로팅 윈도우 드래그 시작
|
||||||
|
startDragging(event) {
|
||||||
|
this.isDragging = true;
|
||||||
|
this.dragOffset.x = event.clientX - this.floatingMemoPosition.x;
|
||||||
|
this.dragOffset.y = event.clientY - this.floatingMemoPosition.y;
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', this.handleDragging.bind(this));
|
||||||
|
document.addEventListener('mouseup', this.stopDragging.bind(this));
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 드래그 중
|
||||||
|
handleDragging(event) {
|
||||||
|
if (!this.isDragging) return;
|
||||||
|
|
||||||
|
const newX = event.clientX - this.dragOffset.x;
|
||||||
|
const newY = event.clientY - this.dragOffset.y;
|
||||||
|
|
||||||
|
// 화면 경계 제한
|
||||||
|
const maxX = window.innerWidth - this.floatingMemoSize.width;
|
||||||
|
const maxY = window.innerHeight - this.floatingMemoSize.height;
|
||||||
|
|
||||||
|
this.floatingMemoPosition.x = Math.max(0, Math.min(newX, maxX));
|
||||||
|
this.floatingMemoPosition.y = Math.max(0, Math.min(newY, maxY));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 드래그 종료
|
||||||
|
stopDragging() {
|
||||||
|
this.isDragging = false;
|
||||||
|
document.removeEventListener('mousemove', this.handleDragging.bind(this));
|
||||||
|
document.removeEventListener('mouseup', this.stopDragging.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 리사이즈 시작
|
||||||
|
startResizing(event) {
|
||||||
|
this.isResizing = true;
|
||||||
|
document.addEventListener('mousemove', this.handleResizing.bind(this));
|
||||||
|
document.addEventListener('mouseup', this.stopResizing.bind(this));
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 리사이즈 중
|
||||||
|
handleResizing(event) {
|
||||||
|
if (!this.isResizing) return;
|
||||||
|
|
||||||
|
const rect = event.target.closest('.fixed').getBoundingClientRect();
|
||||||
|
const newWidth = event.clientX - rect.left;
|
||||||
|
const newHeight = event.clientY - rect.top;
|
||||||
|
|
||||||
|
// 최소/최대 크기 제한
|
||||||
|
this.floatingMemoSize.width = Math.max(300, Math.min(newWidth, 800));
|
||||||
|
this.floatingMemoSize.height = Math.max(200, Math.min(newHeight, 600));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 리사이즈 종료
|
||||||
|
stopResizing() {
|
||||||
|
this.isResizing = false;
|
||||||
|
document.removeEventListener('mousemove', this.handleResizing.bind(this));
|
||||||
|
document.removeEventListener('mouseup', this.stopResizing.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 플로팅 메모창 크기 토글
|
||||||
|
toggleFloatingMemoSize() {
|
||||||
|
if (this.floatingMemoSize.width === 400) {
|
||||||
|
// 큰 크기로
|
||||||
|
this.floatingMemoSize.width = 600;
|
||||||
|
this.floatingMemoSize.height = 700;
|
||||||
|
} else {
|
||||||
|
// 기본 크기로
|
||||||
|
this.floatingMemoSize.width = 400;
|
||||||
|
this.floatingMemoSize.height = 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -313,8 +313,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 스크립트 -->
|
<!-- 스크립트 -->
|
||||||
<script src="/static/js/api.js"></script>
|
<script src="/static/js/api.js?v=2025012225"></script>
|
||||||
<script src="/static/js/viewer.js"></script>
|
<script src="/static/js/viewer.js?v=2025012225"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
|
|||||||
Reference in New Issue
Block a user