feat: 완전한 문서 관리 시스템 구현

 주요 기능:
- 문서 사라짐 문제 해결: API limit 제한으로 인한 문서 누락 해결
- 서적별 문서 관리: HTML과 PDF 통합 관리 시스템
- PDF 뷰어 개선: 인증, 네비게이션, 에러 처리 강화
- 서적 편집/삭제: 완전한 서적 관리 기능

🔧 기술적 개선:
- /api/documents/all 엔드포인트 추가 (모든 문서 조회)
- HTML/PDF 문서 타입별 아이콘 및 필터링
- 서적별 뷰에서 편집/삭제 버튼 추가
- PDF Manager와 서적 편집 페이지 연동

🎨 UI/UX 개선:
- Devonthink 스타일 서적 그룹화
- HTML 문서 순서 관리와 PDF 관리 섹션 분리
- 문서 타입별 시각적 구분 (HTML: 파란색, PDF: 빨간색)
- 2단계 확인을 통한 안전한 서적 삭제

�� 버그 수정:
- PDF 삭제 시 undefined ID 전달 문제 해결
- 서적 편집 페이지 422 오류 해결 (URL 파라미터 문제)
- PDF.js 워커 설정 및 인증 토큰 처리 개선
This commit is contained in:
hyungi
2025-09-05 11:00:17 +09:00
parent cfb9485d4f
commit 6a537008db
85 changed files with 375 additions and 28 deletions

View File

@@ -83,12 +83,13 @@ router = APIRouter()
@router.get("/", response_model=List[DocumentResponse])
async def list_documents(
skip: int = 0,
limit: int = 50,
limit: int = 50, # 기본값 복원
tag: Optional[str] = None,
search: Optional[str] = None,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""페이지네이션이 있는 문서 목록 조회"""
"""문서 목록 조회"""
query = select(Document).options(
selectinload(Document.uploader),
@@ -160,6 +161,70 @@ async def list_documents(
return response_data
@router.get("/all", response_model=List[DocumentResponse])
async def list_all_documents(
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""모든 문서 조회 (페이지네이션 없음) - 프론트엔드 전용"""
query = select(Document).options(
selectinload(Document.uploader),
selectinload(Document.tags),
selectinload(Document.book), # 서적 정보 추가
selectinload(Document.category) # 소분류 정보 추가
)
# 권한 필터링 (관리자가 아니면 공개 문서 + 자신이 업로드한 문서만)
if not current_user.is_admin:
query = query.where(
or_(
Document.is_public == True,
Document.uploaded_by == current_user.id
)
)
query = query.order_by(Document.created_at.desc())
result = await db.execute(query)
documents = result.scalars().all()
# 응답 데이터 변환
response_data = []
for doc in documents:
doc_data = DocumentResponse(
id=str(doc.id),
title=doc.title,
description=doc.description,
html_path=doc.html_path, # None 가능 (PDF만 업로드한 경우)
pdf_path=doc.pdf_path,
thumbnail_path=doc.thumbnail_path,
file_size=doc.file_size,
page_count=doc.page_count,
language=doc.language,
is_public=doc.is_public,
is_processed=doc.is_processed,
created_at=doc.created_at,
updated_at=doc.updated_at,
document_date=doc.document_date,
uploader_name=doc.uploader.full_name or doc.uploader.email,
tags=[tag.name for tag in doc.tags],
# 서적 정보 추가
book_id=str(doc.book.id) if doc.book else None,
book_title=doc.book.title if doc.book else None,
book_author=doc.book.author if doc.book else None,
# 소분류 정보 추가
category_id=str(doc.category.id) if doc.category else None,
category_name=doc.category.name if doc.category else None,
sort_order=doc.sort_order,
# PDF 매칭 정보 추가
matched_pdf_id=str(doc.matched_pdf_id) if doc.matched_pdf_id else None
)
response_data.append(doc_data)
return response_data
@router.get("/hierarchy/structured", response_model=dict)
async def get_documents_by_hierarchy(
current_user: User = Depends(get_current_active_user),
@@ -375,10 +440,14 @@ async def upload_document(
await db.commit()
# 문서 정보를 다시 로드 (태그 포함)
# 문서 정보를 다시 로드 (태그, 서적, 카테고리 포함)
result = await db.execute(
select(Document)
.options(selectinload(Document.tags))
.options(
selectinload(Document.tags),
selectinload(Document.book),
selectinload(Document.category)
)
.where(Document.id == document.id)
)
document_with_tags = result.scalar_one()
@@ -401,6 +470,14 @@ async def upload_document(
document_date=document_with_tags.document_date,
uploader_name=current_user.full_name or current_user.email,
tags=[tag.name for tag in document_with_tags.tags],
# 서적 정보 추가
book_id=str(document_with_tags.book.id) if document_with_tags.book else None,
book_title=document_with_tags.book.title if document_with_tags.book else None,
book_author=document_with_tags.book.author if document_with_tags.book else None,
# 소분류 정보 추가
category_id=str(document_with_tags.category.id) if document_with_tags.category else None,
category_name=document_with_tags.category.name if document_with_tags.category else None,
sort_order=document_with_tags.sort_order,
matched_pdf_id=str(document_with_tags.matched_pdf_id) if document_with_tags.matched_pdf_id else None
)

View File

@@ -95,16 +95,29 @@
rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"></textarea>
</div>
<!-- 서적 관리 버튼들 -->
<div class="flex justify-between pt-4 border-t border-gray-200">
<button @click="deleteBook()"
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
<i class="fas fa-trash mr-2"></i>서적 삭제
</button>
<button @click="saveBookInfo()"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<i class="fas fa-save mr-2"></i>서적 정보 저장
</button>
</div>
</div>
</div>
<!-- 문서 순서 및 PDF 매칭 편집 -->
<!-- HTML 문서 순서 및 PDF 매칭 편집 -->
<div class="bg-white rounded-lg shadow-sm border">
<div class="p-6 border-b border-gray-200">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
<i class="fas fa-list-ol mr-2 text-green-600"></i>
문서 순서 및 PDF 매칭
<i class="fas fa-list-ol mr-2 text-blue-600"></i>
HTML 문서 순서 및 PDF 매칭
</h2>
<div class="flex space-x-2">
<button @click="autoSortByName()"
@@ -138,15 +151,22 @@
<!-- 문서 정보 -->
<div class="flex-1">
<h3 class="font-medium text-gray-900" x-text="doc.title"></h3>
<div class="flex items-center space-x-2">
<!-- 문서 타입 아이콘 -->
<i x-show="doc.html_path && !doc.pdf_path" class="fas fa-file-alt text-blue-500" title="HTML 문서"></i>
<i x-show="doc.pdf_path && !doc.html_path" class="fas fa-file-pdf text-red-500" title="PDF 문서"></i>
<i x-show="doc.html_path && doc.pdf_path" class="fas fa-file-archive text-purple-500" title="HTML + PDF"></i>
<h3 class="font-medium text-gray-900" x-text="doc.title"></h3>
</div>
<p class="text-sm text-gray-500" x-text="doc.description || '설명 없음'"></p>
</div>
</div>
<!-- PDF 매칭 및 컨트롤 -->
<div class="flex items-center space-x-3">
<!-- PDF 매칭 드롭다운 -->
<div class="min-w-48 relative">
<!-- PDF 매칭 드롭다운 (HTML 문서만) -->
<div x-show="doc.html_path" class="min-w-48 relative">
<select x-model="doc.matched_pdf_id"
:class="doc.matched_pdf_id ? 'border-green-300 bg-green-50' : 'border-gray-300'"
class="w-full px-3 py-2 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm">
@@ -187,6 +207,67 @@
</div>
</div>
</div>
<!-- PDF 전용 문서 관리 -->
<div class="bg-white rounded-lg shadow-sm border">
<div class="p-6 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
<i class="fas fa-file-pdf mr-2 text-red-600"></i>
등록된 PDF 문서 관리
<span class="ml-2 px-2 py-1 bg-red-100 text-red-700 text-xs rounded-full" x-text="pdfDocuments.length + '개'"></span>
</h2>
</div>
<div class="p-6">
<!-- PDF 문서 목록 -->
<div class="space-y-3">
<template x-for="(pdf, index) in pdfDocuments" :key="pdf.id">
<div class="bg-red-50 border border-red-200 rounded-lg p-4 hover:bg-red-100 transition-colors">
<div class="flex items-center justify-between">
<div class="flex items-center flex-1">
<!-- PDF 아이콘 -->
<div class="w-10 h-10 bg-red-600 text-white rounded-full flex items-center justify-center text-sm font-medium mr-4">
<i class="fas fa-file-pdf"></i>
</div>
<!-- PDF 정보 -->
<div class="flex-1">
<div class="flex items-center space-x-2">
<h3 class="font-medium text-gray-900" x-text="pdf.title"></h3>
<span class="px-2 py-1 bg-red-100 text-red-700 text-xs rounded-full">PDF 전용</span>
</div>
<p class="text-sm text-gray-500" x-text="pdf.description || '설명 없음'"></p>
<div class="flex items-center space-x-4 text-xs text-gray-400 mt-1">
<span x-text="pdf.original_filename"></span>
<span x-text="new Date(pdf.created_at).toLocaleDateString()"></span>
<span x-show="pdf.file_size" x-text="Math.round(pdf.file_size / 1024) + 'KB'"></span>
</div>
</div>
</div>
<!-- PDF 관리 버튼들 -->
<div class="flex items-center space-x-2">
<button @click="previewPDF(pdf)"
class="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition-colors text-sm">
<i class="fas fa-eye mr-1"></i>미리보기
</button>
<button @click="deletePDF(pdf)"
class="px-3 py-1.5 bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors text-sm">
<i class="fas fa-trash mr-1"></i>삭제
</button>
</div>
</div>
</div>
</template>
</div>
<!-- 빈 상태 -->
<div x-show="pdfDocuments.length === 0" class="text-center py-8">
<i class="fas fa-file-pdf text-gray-400 text-3xl mb-4"></i>
<p class="text-gray-500">등록된 PDF 문서가 없습니다</p>
</div>
</div>
</div>
</div>
</main>

View File

@@ -150,7 +150,10 @@
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 line-clamp-2" x-text="doc.title"></h3>
<i class="fas fa-file-alt text-blue-500 text-xl"></i>
<!-- 문서 타입별 아이콘 -->
<i x-show="doc.html_path && !doc.pdf_path" class="fas fa-file-alt text-blue-500 text-xl" title="HTML 문서"></i>
<i x-show="doc.pdf_path && !doc.html_path" class="fas fa-file-pdf text-red-500 text-xl" title="PDF 문서"></i>
<i x-show="doc.html_path && doc.pdf_path" class="fas fa-file-archive text-purple-500 text-xl" title="HTML + PDF"></i>
</div>
<p class="text-gray-600 text-sm mb-3 line-clamp-3" x-text="doc.description || '설명 없음'"></p>
@@ -222,11 +225,22 @@
</div>
</div>
<!-- 확장/축소 아이콘 -->
<!-- 서적 관리 버튼들 -->
<div class="flex items-center space-x-2">
<button @click.stop="openBookDocuments(bookGroup.book)"
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors">
편집
<!-- 서적 편집 버튼 -->
<button x-show="bookGroup.book?.id"
@click.stop="window.location.href = `/book-editor.html?id=${bookGroup.book.id}`"
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition-colors"
title="서적 편집">
<i class="fas fa-edit mr-1"></i>편집
</button>
<!-- 서적 삭제 버튼 -->
<button x-show="bookGroup.book?.id"
@click.stop="deleteBook(bookGroup.book)"
class="px-3 py-1 text-xs bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors"
title="서적 삭제">
<i class="fas fa-trash mr-1"></i>삭제
</button>
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-200"
:class="{'rotate-180': bookGroup.expanded}"></i>
@@ -243,8 +257,17 @@
<div class="flex items-center space-x-3 flex-1">
<!-- 문서 타입 아이콘 -->
<div class="w-8 h-8 rounded-md flex items-center justify-center"
:class="doc.pdf_path ? 'bg-red-100 text-red-600' : 'bg-gray-100 text-gray-600'">
<i :class="doc.pdf_path ? 'fas fa-file-pdf' : 'fas fa-file-alt'" class="text-xs"></i>
:class="{
'bg-blue-100 text-blue-600': doc.html_path && !doc.pdf_path,
'bg-red-100 text-red-600': doc.pdf_path && !doc.html_path,
'bg-purple-100 text-purple-600': doc.html_path && doc.pdf_path
}">
<i class="text-xs"
:class="{
'fas fa-file-alt': doc.html_path && !doc.pdf_path,
'fas fa-file-pdf': doc.pdf_path && !doc.html_path,
'fas fa-file-archive': doc.html_path && doc.pdf_path
}"></i>
</div>
<!-- 문서 정보 -->

View File

@@ -279,8 +279,16 @@
</div>
</div>
<!-- 확장/축소 아이콘 -->
<!-- 서적 관리 버튼들 -->
<div class="flex items-center space-x-2">
<!-- 서적 편집 버튼 -->
<button x-show="bookGroup.book?.id"
@click.stop="window.location.href = `/book-editor.html?id=${bookGroup.book.id}`"
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition-colors"
title="서적 편집">
<i class="fas fa-edit mr-1"></i>편집
</button>
<span class="text-xs text-gray-500" x-text="bookGroup.pdfs.length + '개'"></span>
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-200"
:class="{'rotate-180': bookGroup.expanded}"></i>
@@ -319,7 +327,7 @@
title="다운로드">
<i class="fas fa-download text-xs"></i>
</button>
<button @click.stop="deletePDF(pdf.id)"
<button @click.stop="deletePDF(pdf)"
class="p-2 text-gray-400 hover:text-red-600 transition-colors rounded-md hover:bg-red-50"
title="삭제">
<i class="fas fa-trash text-xs"></i>

View File

@@ -213,6 +213,11 @@ class DocumentServerAPI {
return await this.get('/documents/', params);
}
async getAllDocuments() {
// 모든 문서를 가져오는 전용 엔드포인트
return await this.get('/documents/all');
}
async getDocumentsHierarchy() {
return await this.get('/documents/hierarchy/structured');
}
@@ -429,6 +434,14 @@ class DocumentServerAPI {
return await this.post('/books', bookData);
}
async updateBook(bookId, bookData) {
return await this.put(`/books/${bookId}`, bookData);
}
async deleteBook(bookId) {
return await this.delete(`/books/${bookId}`);
}
async getBook(bookId) {
return await this.get(`/books/${bookId}`);
}

View File

@@ -75,7 +75,7 @@ window.bookDocumentsApp = () => ({
try {
// 모든 문서 가져오기
const allDocuments = await window.api.getDocuments();
const allDocuments = await window.api.getAllDocuments();
if (this.bookId === 'none') {
// 서적 미분류 HTML 문서들만 (폴더로 구분)

View File

@@ -1,7 +1,8 @@
// 서적 편집 애플리케이션 컴포넌트
window.bookEditorApp = () => ({
// 상태 관리
documents: [],
documents: [], // HTML 문서들 (순서 관리용)
pdfDocuments: [], // PDF 전용 문서들 (별도 관리용)
bookInfo: {},
availablePDFs: [],
loading: false,
@@ -40,8 +41,9 @@ window.bookEditorApp = () => ({
// URL 파라미터 파싱
parseUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
this.bookId = urlParams.get('bookId');
this.bookId = urlParams.get('id') || urlParams.get('bookId'); // 'id' 또는 'bookId' 파라미터 지원
console.log('📖 편집할 서적 ID:', this.bookId);
console.log('📖 URL 파라미터들:', Object.fromEntries(urlParams));
},
// 인증 상태 확인
@@ -74,21 +76,35 @@ window.bookEditorApp = () => ({
this.error = '';
try {
console.log('🔍 서적 ID 확인:', this.bookId);
// 서적 정보 로드
this.bookInfo = await window.api.getBook(this.bookId);
console.log('📚 서적 정보 로드:', this.bookInfo);
// 모든 문서 가져와서 이 서적에 속한 HTML 문서들 필터링 (폴더로 구분)
const allDocuments = await window.api.getDocuments();
// 모든 문서 가져와서 이 서적에 속한 문서들 필터링
const allDocuments = await window.api.getAllDocuments();
// HTML 문서만 (순서 관리용)
this.documents = allDocuments
.filter(doc =>
doc.book_id === this.bookId &&
doc.html_path &&
doc.html_path.includes('/documents/') // HTML은 documents 폴더에 저장됨
doc.html_path &&
doc.html_path.includes('/documents/')
)
.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); // 순서대로 정렬
console.log('📄 서적 문서들:', this.documents.length, '개');
// PDF 전용 문서들 (별도 관리용)
this.pdfDocuments = allDocuments
.filter(doc =>
doc.book_id === this.bookId &&
doc.pdf_path &&
!doc.html_path // PDF만 있는 문서
)
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); // 최신순
console.log('📄 HTML 문서들:', this.documents.length, '개 (순서 관리)');
console.log('📕 PDF 전용 문서들:', this.pdfDocuments.length, '개 (별도 관리)');
// 각 문서의 PDF 매칭 상태 확인
this.documents.forEach((doc, index) => {
@@ -295,6 +311,93 @@ window.bookEditorApp = () => ({
}
},
// 서적 정보 저장
async saveBookInfo() {
try {
await window.api.updateBook(this.bookId, {
title: this.bookInfo.title,
author: this.bookInfo.author,
description: this.bookInfo.description
});
this.showNotification('서적 정보가 저장되었습니다', 'success');
} catch (error) {
console.error('서적 정보 저장 실패:', error);
this.showNotification('서적 정보 저장에 실패했습니다: ' + error.message, 'error');
}
},
// 서적 삭제 (모든 문서 포함)
async deleteBook() {
if (!this.bookInfo.title) {
alert('서적 정보가 로드되지 않았습니다.');
return;
}
const confirmMessage = `"${this.bookInfo.title}" 서적을 완전히 삭제하시겠습니까?\n\n⚠️ 경고: 이 작업은 되돌릴 수 없습니다!\n\n삭제될 항목:\n- 서적 정보\n- HTML 문서 ${this.documents.length}\n- PDF 전용 문서 ${this.pdfDocuments.length}\n- 관련된 모든 하이라이트, 노트, 링크`;
if (!confirm(confirmMessage)) {
return;
}
// 한 번 더 확인
const finalConfirm = prompt(`정말로 삭제하시려면 서적 제목을 입력하세요:\n"${this.bookInfo.title}"`);
if (finalConfirm !== this.bookInfo.title) {
alert('서적 제목이 일치하지 않습니다. 삭제가 취소되었습니다.');
return;
}
try {
// 서적에 속한 모든 HTML 문서 삭제
for (const doc of this.documents) {
console.log(`🗑️ HTML 문서 삭제 중: ${doc.title}`);
await window.api.deleteDocument(doc.id);
}
// 서적에 속한 모든 PDF 전용 문서 삭제
for (const doc of this.pdfDocuments) {
console.log(`🗑️ PDF 문서 삭제 중: ${doc.title}`);
await window.api.deleteDocument(doc.id);
}
// 서적 삭제
await window.api.deleteBook(this.bookId);
alert('서적이 완전히 삭제되었습니다.');
// 메인 페이지로 이동
window.location.href = '/index.html';
} catch (error) {
console.error('서적 삭제 실패:', error);
alert('서적 삭제에 실패했습니다: ' + error.message);
}
},
// PDF 미리보기
previewPDF(pdf) {
// PDF 뷰어 페이지로 이동
window.open(`/viewer.html?id=${pdf.id}`, '_blank');
},
// PDF 삭제
async deletePDF(pdf) {
if (!confirm(`"${pdf.title}" PDF 문서를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.`)) {
return;
}
try {
await window.api.deleteDocument(pdf.id);
// PDF 목록에서 제거
this.pdfDocuments = this.pdfDocuments.filter(p => p.id !== pdf.id);
this.showNotification('PDF 문서가 삭제되었습니다', 'success');
} catch (error) {
console.error('PDF 삭제 실패:', error);
this.showNotification('PDF 삭제에 실패했습니다: ' + error.message, 'error');
}
},
// 뒤로가기
goBack() {
window.location.href = `book-documents.html?bookId=${this.bookId}`;

View File

@@ -134,7 +134,10 @@ window.documentApp = () => ({
this.error = '';
try {
const allDocuments = await window.api.getDocuments();
const allDocuments = await window.api.getAllDocuments();
// 디버깅: API 응답 원본 확인
console.log('🔍 API 응답 원본 (첫 3개):', JSON.stringify(allDocuments.slice(0, 3), null, 2));
// HTML 문서만 필터링 (PDF 파일 제외)
this.documents = allDocuments.filter(doc =>
@@ -146,6 +149,19 @@ window.documentApp = () => ({
console.log('📄 HTML 문서:', this.documents.length, '개');
console.log('📄 PDF 파일:', allDocuments.length - this.documents.length, '개 (제외됨)');
// 디버깅: 사라진 문서 찾기
console.log('🔍 HTML 경로가 있는 문서들:');
allDocuments.forEach(doc => {
if (doc.html_path) {
console.log(` - ${doc.title}: ${doc.html_path}`);
}
});
console.log('🔍 필터링된 문서들:');
this.documents.forEach(doc => {
console.log(` - ${doc.title}: ${doc.html_path}`);
});
this.updateAvailableTags();
this.filterDocuments();
this.syncUIState(); // UI 상태 동기화
@@ -206,6 +222,32 @@ window.documentApp = () => ({
this.filterDocuments();
},
// 서적 삭제
async deleteBook(book) {
if (!book || !book.id) {
alert('서적 정보가 올바르지 않습니다.');
return;
}
const confirmMessage = `"${book.title}" 서적을 삭제하시겠습니까?\n\n⚠️ 주의: 이 서적에 속한 모든 문서들이 '서적 미분류'로 이동됩니다.`;
if (!confirm(confirmMessage)) {
return;
}
try {
await window.api.deleteBook(book.id);
// 문서 목록 다시 로드
await this.loadDocuments();
alert('서적이 삭제되었습니다.');
} catch (error) {
console.error('서적 삭제 실패:', error);
alert('서적 삭제에 실패했습니다: ' + error.message);
}
},
// 필터 초기화
clearFilters() {
this.searchQuery = '';

View File

@@ -67,7 +67,7 @@ window.pdfManagerApp = () => ({
try {
// 모든 문서 가져오기
this.allDocuments = await window.api.getDocuments();
this.allDocuments = await window.api.getAllDocuments();
// PDF 파일들만 필터링
this.pdfDocuments = this.allDocuments.filter(doc =>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.