Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Update app.py
Browse files
app.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
-
from fastapi import FastAPI, BackgroundTasks, UploadFile, File, Form
|
2 |
-
from fastapi.responses import HTMLResponse, JSONResponse, Response
|
3 |
from fastapi.staticfiles import StaticFiles
|
4 |
-
import pathlib, os, uvicorn, base64, json, shutil
|
5 |
-
from typing import Dict, List, Any
|
6 |
import asyncio
|
7 |
import logging
|
8 |
import threading
|
@@ -31,6 +31,12 @@ CACHE_DIR = BASE / "cache"
|
|
31 |
if not CACHE_DIR.exists():
|
32 |
CACHE_DIR.mkdir(parents=True)
|
33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
# 관리자 비밀번호
|
35 |
ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # 환경 변수에서 가져오기, 기본값은 테스트용
|
36 |
|
@@ -38,6 +44,42 @@ ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # 환경 변수에서 가져오
|
|
38 |
pdf_cache: Dict[str, Dict[str, Any]] = {}
|
39 |
# 캐싱 락
|
40 |
cache_locks = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
# PDF 파일 목록 가져오기 (메인 디렉토리용)
|
43 |
def get_pdf_files():
|
@@ -53,7 +95,6 @@ def get_permanent_pdf_files():
|
|
53 |
pdf_files = [f for f in PERMANENT_PDF_DIR.glob("*.pdf")]
|
54 |
return pdf_files
|
55 |
|
56 |
-
# PDF 썸네일 생성 및 프로젝트 데이터 준비
|
57 |
# PDF 썸네일 생성 및 프로젝트 데이터 준비
|
58 |
def generate_pdf_projects():
|
59 |
projects_data = []
|
@@ -75,15 +116,28 @@ def generate_pdf_projects():
|
|
75 |
|
76 |
# 중복 제거된 파일들로 프로젝트 데이터 생성
|
77 |
for pdf_file in unique_files.values():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
projects_data.append({
|
79 |
"path": str(pdf_file),
|
80 |
"name": pdf_file.stem,
|
|
|
81 |
"cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
|
82 |
})
|
83 |
|
84 |
return projects_data
|
85 |
|
86 |
-
|
87 |
# 캐시 파일 경로 생성
|
88 |
def get_cache_path(pdf_name: str):
|
89 |
return CACHE_DIR / f"{pdf_name}_cache.json"
|
@@ -242,10 +296,40 @@ async def cache_pdf(pdf_path: str):
|
|
242 |
pdf_cache[pdf_name]["status"] = "error"
|
243 |
pdf_cache[pdf_name]["error"] = str(e)
|
244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
245 |
# 시작 시 모든 PDF 파일 캐싱
|
246 |
async def init_cache_all_pdfs():
|
247 |
logger.info("PDF 캐싱 작업 시작")
|
248 |
|
|
|
|
|
|
|
249 |
# 메인 및 영구 디렉토리에서 PDF 파일 모두 가져오기
|
250 |
pdf_files = get_pdf_files() + get_permanent_pdf_files()
|
251 |
|
@@ -253,6 +337,25 @@ async def init_cache_all_pdfs():
|
|
253 |
unique_pdf_paths = set(str(p) for p in pdf_files)
|
254 |
pdf_files = [pathlib.Path(p) for p in unique_pdf_paths]
|
255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
256 |
# 이미 캐시된 PDF 파일 로드 (빠른 시작을 위해 먼저 수행)
|
257 |
for cache_file in CACHE_DIR.glob("*_cache.json"):
|
258 |
try:
|
@@ -290,14 +393,43 @@ async def get_permanent_pdf_projects_api():
|
|
290 |
projects_data = []
|
291 |
|
292 |
for pdf_file in pdf_files:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
293 |
projects_data.append({
|
294 |
"path": str(pdf_file),
|
295 |
"name": pdf_file.stem,
|
|
|
296 |
"cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
|
297 |
})
|
298 |
|
299 |
return projects_data
|
300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
301 |
# API 엔드포인트: PDF 썸네일 제공 (최적화)
|
302 |
@app.get("/api/pdf-thumbnail")
|
303 |
async def get_pdf_thumbnail(path: str):
|
@@ -422,7 +554,6 @@ async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
|
|
422 |
logger.error(f"PDF 콘텐츠 로드 오류: {str(e)}\n{error_details}")
|
423 |
return JSONResponse(content={"error": str(e)}, status_code=500)
|
424 |
|
425 |
-
|
426 |
# PDF 업로드 엔드포인트 - 영구 저장소에 저장 및 메인 화면에 자동 표시
|
427 |
@app.post("/api/upload-pdf")
|
428 |
async def upload_pdf(file: UploadFile = File(...)):
|
@@ -446,11 +577,22 @@ async def upload_pdf(file: UploadFile = File(...)):
|
|
446 |
with open(PDF_DIR / file.filename, "wb") as buffer:
|
447 |
buffer.write(content)
|
448 |
|
|
|
|
|
|
|
|
|
|
|
449 |
# 백그라운드에서 캐싱 시작
|
450 |
asyncio.create_task(cache_pdf(str(file_path)))
|
451 |
|
452 |
return JSONResponse(
|
453 |
-
content={
|
|
|
|
|
|
|
|
|
|
|
|
|
454 |
status_code=200
|
455 |
)
|
456 |
except Exception as e:
|
@@ -462,7 +604,6 @@ async def upload_pdf(file: UploadFile = File(...)):
|
|
462 |
status_code=500
|
463 |
)
|
464 |
|
465 |
-
|
466 |
# 관리자 인증 엔드포인트
|
467 |
@app.post("/api/admin-login")
|
468 |
async def admin_login(password: str = Form(...)):
|
@@ -491,6 +632,17 @@ async def delete_pdf(path: str):
|
|
491 |
if pdf_name in pdf_cache:
|
492 |
del pdf_cache[pdf_name]
|
493 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
return {"success": True}
|
495 |
except Exception as e:
|
496 |
logger.error(f"PDF 삭제 오류: {str(e)}")
|
@@ -528,13 +680,67 @@ async def unfeature_pdf(path: str):
|
|
528 |
logger.error(f"PDF 표시 해제 오류: {str(e)}")
|
529 |
return {"success": False, "message": str(e)}
|
530 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
531 |
# HTML 파일 읽기 함수
|
532 |
-
def get_html_content():
|
533 |
html_path = BASE / "flipbook_template.html"
|
|
|
534 |
if html_path.exists():
|
535 |
with open(html_path, "r", encoding="utf-8") as f:
|
536 |
-
|
537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
538 |
|
539 |
# HTML 문자열 (UI 수정 버전)
|
540 |
HTML = """
|
@@ -1298,6 +1504,9 @@ HTML = """
|
|
1298 |
// 현재 페이지 로딩 상태
|
1299 |
let currentLoadingPdfPath = null;
|
1300 |
let pageLoadingInterval = null;
|
|
|
|
|
|
|
1301 |
|
1302 |
/* 🔊 오디오 unlock – 내장 Audio 와 같은 MP3 경로 사용 */
|
1303 |
['click','touchstart'].forEach(evt=>{
|
@@ -1371,7 +1580,7 @@ HTML = """
|
|
1371 |
await loadServerPDFs();
|
1372 |
|
1373 |
// 성공 메시지
|
1374 |
-
showMessage("PDF가 성공적으로 업로드되었습니다!");
|
1375 |
} else {
|
1376 |
hideLoading();
|
1377 |
showError("업로드 실패: " + (result.message || "알 수 없는 오류"));
|
@@ -1383,26 +1592,31 @@ HTML = """
|
|
1383 |
}
|
1384 |
}
|
1385 |
|
1386 |
-
function addCard(i, thumb, title, isCached = false) {
|
1387 |
const d = document.createElement('div');
|
1388 |
d.className = 'card fade-in';
|
1389 |
d.onclick = () => open(i);
|
1390 |
|
|
|
|
|
|
|
|
|
|
|
1391 |
// 제목 처리
|
1392 |
const displayTitle = title ?
|
1393 |
-
|
1394 |
-
|
1395 |
|
1396 |
// 캐시 상태 뱃지 추가
|
1397 |
const cachedBadge = isCached ?
|
1398 |
-
|
1399 |
|
1400 |
d.innerHTML = `
|
1401 |
-
|
1402 |
-
|
1403 |
-
|
1404 |
-
|
1405 |
-
|
1406 |
`;
|
1407 |
grid.appendChild(d);
|
1408 |
|
@@ -1411,9 +1625,9 @@ HTML = """
|
|
1411 |
}
|
1412 |
|
1413 |
/* ── 프로젝트 저장 ── */
|
1414 |
-
function save(pages, title, isCached = false){
|
1415 |
-
const id=projects.push(pages)-1;
|
1416 |
-
addCard(id, pages[0].thumb, title, isCached);
|
1417 |
}
|
1418 |
|
1419 |
/* ── 서버 PDF 로드 및 캐시 상태 확인 ── */
|
@@ -1465,7 +1679,8 @@ HTML = """
|
|
1465 |
return {
|
1466 |
pages,
|
1467 |
name: project.name,
|
1468 |
-
isCached
|
|
|
1469 |
};
|
1470 |
}
|
1471 |
} catch (err) {
|
@@ -1480,7 +1695,7 @@ HTML = """
|
|
1480 |
|
1481 |
// 성공적으로 가져온 결과만 표시
|
1482 |
results.filter(result => result !== null).forEach(result => {
|
1483 |
-
save(result.pages, result.name, result.isCached);
|
1484 |
});
|
1485 |
|
1486 |
hideLoading();
|
@@ -1565,11 +1780,120 @@ HTML = """
|
|
1565 |
}
|
1566 |
}
|
1567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1568 |
/* ── 카드 → FlipBook ── */
|
1569 |
async function open(i) {
|
1570 |
toggle(false);
|
1571 |
const pages = projects[i];
|
1572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1573 |
// 기존 FlipBook 정리
|
1574 |
if(fb) {
|
1575 |
fb.destroy();
|
@@ -1833,7 +2157,7 @@ HTML = """
|
|
1833 |
enableDownload: false,
|
1834 |
enablePrint: false,
|
1835 |
enableZoom: true,
|
1836 |
-
enableShare:
|
1837 |
enableSearch: true,
|
1838 |
enableAutoPlay: true,
|
1839 |
enableAnnotation: false,
|
@@ -1850,7 +2174,8 @@ HTML = """
|
|
1850 |
pageTextureSize: 1024, // 페이지 텍스처 크기
|
1851 |
thumbnails: true, // 섬네일 활성화
|
1852 |
autoHideControls: false, // 자동 숨김 비활성화
|
1853 |
-
controlsTimeout: 8000 // 컨트롤 표시 시간 연장
|
|
|
1854 |
}
|
1855 |
});
|
1856 |
|
@@ -1905,6 +2230,7 @@ HTML = """
|
|
1905 |
}
|
1906 |
$id('loadingPages').style.display = 'none';
|
1907 |
currentLoadingPdfPath = null;
|
|
|
1908 |
};
|
1909 |
|
1910 |
function toggle(showHome){
|
@@ -2019,12 +2345,18 @@ HTML = """
|
|
2019 |
const card = document.createElement('div');
|
2020 |
card.className = 'admin-card card fade-in';
|
2021 |
|
|
|
|
|
|
|
2022 |
// 썸네일 및 정보
|
2023 |
card.innerHTML = `
|
2024 |
<div class="card-inner">
|
2025 |
${pdf.cached ? '<div class="cached-status">캐시됨</div>' : ''}
|
2026 |
<img src="${thumbData.thumbnail || ''}" alt="${pdf.name}" loading="lazy">
|
2027 |
<p title="${pdf.name}">${pdf.name.length > 15 ? pdf.name.substring(0, 15) + '...' : pdf.name}</p>
|
|
|
|
|
|
|
2028 |
${isMainDisplayed ?
|
2029 |
`<button class="unfeature-btn" data-path="${pdf.path}">메인에서 제거</button>` :
|
2030 |
`<button class="feature-btn" data-path="${pdf.path}">메인에 표시</button>`}
|
@@ -2279,9 +2611,5 @@ HTML = """
|
|
2279 |
</html>
|
2280 |
"""
|
2281 |
|
2282 |
-
@app.get("/", response_class=HTMLResponse)
|
2283 |
-
async def root():
|
2284 |
-
return get_html_content()
|
2285 |
-
|
2286 |
if __name__ == "__main__":
|
2287 |
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
|
|
|
1 |
+
from fastapi import FastAPI, BackgroundTasks, UploadFile, File, Form, Request, Query
|
2 |
+
from fastapi.responses import HTMLResponse, JSONResponse, Response, RedirectResponse
|
3 |
from fastapi.staticfiles import StaticFiles
|
4 |
+
import pathlib, os, uvicorn, base64, json, shutil, uuid, time, urllib.parse
|
5 |
+
from typing import Dict, List, Any, Optional
|
6 |
import asyncio
|
7 |
import logging
|
8 |
import threading
|
|
|
31 |
if not CACHE_DIR.exists():
|
32 |
CACHE_DIR.mkdir(parents=True)
|
33 |
|
34 |
+
# PDF 메타데이터 디렉토리 및 파일 설정
|
35 |
+
METADATA_DIR = pathlib.Path("/data/metadata") if os.path.exists("/data") else BASE / "metadata"
|
36 |
+
if not METADATA_DIR.exists():
|
37 |
+
METADATA_DIR.mkdir(parents=True)
|
38 |
+
PDF_METADATA_FILE = METADATA_DIR / "pdf_metadata.json"
|
39 |
+
|
40 |
# 관리자 비밀번호
|
41 |
ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # 환경 변수에서 가져오기, 기본값은 테스트용
|
42 |
|
|
|
44 |
pdf_cache: Dict[str, Dict[str, Any]] = {}
|
45 |
# 캐싱 락
|
46 |
cache_locks = {}
|
47 |
+
# PDF 메타데이터 (ID to 경로 매핑)
|
48 |
+
pdf_metadata: Dict[str, str] = {}
|
49 |
+
|
50 |
+
# PDF 메타데이터 로드
|
51 |
+
def load_pdf_metadata():
|
52 |
+
global pdf_metadata
|
53 |
+
if PDF_METADATA_FILE.exists():
|
54 |
+
try:
|
55 |
+
with open(PDF_METADATA_FILE, "r") as f:
|
56 |
+
pdf_metadata = json.load(f)
|
57 |
+
logger.info(f"PDF 메타데이터 로드 완료: {len(pdf_metadata)} 항목")
|
58 |
+
except Exception as e:
|
59 |
+
logger.error(f"메타데이터 로드 오류: {e}")
|
60 |
+
pdf_metadata = {}
|
61 |
+
else:
|
62 |
+
pdf_metadata = {}
|
63 |
+
|
64 |
+
# PDF 메타데이터 저장
|
65 |
+
def save_pdf_metadata():
|
66 |
+
try:
|
67 |
+
with open(PDF_METADATA_FILE, "w") as f:
|
68 |
+
json.dump(pdf_metadata, f)
|
69 |
+
except Exception as e:
|
70 |
+
logger.error(f"메타데이터 저장 오류: {e}")
|
71 |
+
|
72 |
+
# PDF ID 생성 (파일명 + 타임스탬프 기반)
|
73 |
+
def generate_pdf_id(filename: str) -> str:
|
74 |
+
# 파일명에서 확장자 제거
|
75 |
+
base_name = os.path.splitext(filename)[0]
|
76 |
+
# URL 안전 문자열로 변환
|
77 |
+
safe_name = urllib.parse.quote(base_name, safe='')
|
78 |
+
# 타임스탬프 추가로 고유성 보장
|
79 |
+
timestamp = int(time.time())
|
80 |
+
# 짧은 임의 문자열 추가
|
81 |
+
random_suffix = uuid.uuid4().hex[:6]
|
82 |
+
return f"{safe_name}_{timestamp}_{random_suffix}"
|
83 |
|
84 |
# PDF 파일 목록 가져오기 (메인 디렉토리용)
|
85 |
def get_pdf_files():
|
|
|
95 |
pdf_files = [f for f in PERMANENT_PDF_DIR.glob("*.pdf")]
|
96 |
return pdf_files
|
97 |
|
|
|
98 |
# PDF 썸네일 생성 및 프로젝트 데이터 준비
|
99 |
def generate_pdf_projects():
|
100 |
projects_data = []
|
|
|
116 |
|
117 |
# 중복 제거된 파일들로 프로젝트 데이터 생성
|
118 |
for pdf_file in unique_files.values():
|
119 |
+
# 해당 파일의 PDF ID 찾기
|
120 |
+
pdf_id = None
|
121 |
+
for pid, path in pdf_metadata.items():
|
122 |
+
if os.path.basename(path) == pdf_file.name:
|
123 |
+
pdf_id = pid
|
124 |
+
break
|
125 |
+
|
126 |
+
# ID가 없으면 새로 생성하고 메타데이터에 추가
|
127 |
+
if not pdf_id:
|
128 |
+
pdf_id = generate_pdf_id(pdf_file.name)
|
129 |
+
pdf_metadata[pdf_id] = str(pdf_file)
|
130 |
+
save_pdf_metadata()
|
131 |
+
|
132 |
projects_data.append({
|
133 |
"path": str(pdf_file),
|
134 |
"name": pdf_file.stem,
|
135 |
+
"id": pdf_id,
|
136 |
"cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
|
137 |
})
|
138 |
|
139 |
return projects_data
|
140 |
|
|
|
141 |
# 캐시 파일 경로 생성
|
142 |
def get_cache_path(pdf_name: str):
|
143 |
return CACHE_DIR / f"{pdf_name}_cache.json"
|
|
|
296 |
pdf_cache[pdf_name]["status"] = "error"
|
297 |
pdf_cache[pdf_name]["error"] = str(e)
|
298 |
|
299 |
+
# PDF ID로 PDF 경로 찾기
|
300 |
+
def get_pdf_path_by_id(pdf_id: str) -> str:
|
301 |
+
if pdf_id in pdf_metadata:
|
302 |
+
path = pdf_metadata[pdf_id]
|
303 |
+
# 파일 존재 확인
|
304 |
+
if os.path.exists(path):
|
305 |
+
return path
|
306 |
+
|
307 |
+
# 영구 저장소에서 파일명으로 찾기
|
308 |
+
filename = os.path.basename(path)
|
309 |
+
perm_path = PERMANENT_PDF_DIR / filename
|
310 |
+
if perm_path.exists():
|
311 |
+
# 메타데이터 업데이트
|
312 |
+
pdf_metadata[pdf_id] = str(perm_path)
|
313 |
+
save_pdf_metadata()
|
314 |
+
return str(perm_path)
|
315 |
+
|
316 |
+
# 메인 디렉토리에서 파일명으로 찾기
|
317 |
+
main_path = PDF_DIR / filename
|
318 |
+
if main_path.exists():
|
319 |
+
# 메타데이터 업데이트
|
320 |
+
pdf_metadata[pdf_id] = str(main_path)
|
321 |
+
save_pdf_metadata()
|
322 |
+
return str(main_path)
|
323 |
+
|
324 |
+
return None
|
325 |
+
|
326 |
# 시작 시 모든 PDF 파일 캐싱
|
327 |
async def init_cache_all_pdfs():
|
328 |
logger.info("PDF 캐싱 작업 시작")
|
329 |
|
330 |
+
# PDF 메타데이터 로드
|
331 |
+
load_pdf_metadata()
|
332 |
+
|
333 |
# 메인 및 영구 디렉토리에서 PDF 파일 모두 가져오기
|
334 |
pdf_files = get_pdf_files() + get_permanent_pdf_files()
|
335 |
|
|
|
337 |
unique_pdf_paths = set(str(p) for p in pdf_files)
|
338 |
pdf_files = [pathlib.Path(p) for p in unique_pdf_paths]
|
339 |
|
340 |
+
# 파일 기반 메타데이터 업데이트
|
341 |
+
for pdf_file in pdf_files:
|
342 |
+
# ID가 없는 파일에 대해 ID 생성
|
343 |
+
found = False
|
344 |
+
for pid, path in pdf_metadata.items():
|
345 |
+
if os.path.basename(path) == pdf_file.name:
|
346 |
+
found = True
|
347 |
+
# 경로 업데이트 필요한 경우
|
348 |
+
if not os.path.exists(path):
|
349 |
+
pdf_metadata[pid] = str(pdf_file)
|
350 |
+
break
|
351 |
+
|
352 |
+
if not found:
|
353 |
+
pdf_id = generate_pdf_id(pdf_file.name)
|
354 |
+
pdf_metadata[pdf_id] = str(pdf_file)
|
355 |
+
|
356 |
+
# 메타데이터 저장
|
357 |
+
save_pdf_metadata()
|
358 |
+
|
359 |
# 이미 캐시된 PDF 파일 로드 (빠른 시작을 위해 먼저 수행)
|
360 |
for cache_file in CACHE_DIR.glob("*_cache.json"):
|
361 |
try:
|
|
|
393 |
projects_data = []
|
394 |
|
395 |
for pdf_file in pdf_files:
|
396 |
+
# PDF ID 찾기
|
397 |
+
pdf_id = None
|
398 |
+
for pid, path in pdf_metadata.items():
|
399 |
+
if os.path.basename(path) == pdf_file.name:
|
400 |
+
pdf_id = pid
|
401 |
+
break
|
402 |
+
|
403 |
+
# ID가 없으면 생성
|
404 |
+
if not pdf_id:
|
405 |
+
pdf_id = generate_pdf_id(pdf_file.name)
|
406 |
+
pdf_metadata[pdf_id] = str(pdf_file)
|
407 |
+
save_pdf_metadata()
|
408 |
+
|
409 |
projects_data.append({
|
410 |
"path": str(pdf_file),
|
411 |
"name": pdf_file.stem,
|
412 |
+
"id": pdf_id,
|
413 |
"cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
|
414 |
})
|
415 |
|
416 |
return projects_data
|
417 |
|
418 |
+
# API 엔드포인트: PDF ID로 정보 가져오기
|
419 |
+
@app.get("/api/pdf-info-by-id/{pdf_id}")
|
420 |
+
async def get_pdf_info_by_id(pdf_id: str):
|
421 |
+
pdf_path = get_pdf_path_by_id(pdf_id)
|
422 |
+
if pdf_path:
|
423 |
+
pdf_file = pathlib.Path(pdf_path)
|
424 |
+
return {
|
425 |
+
"path": pdf_path,
|
426 |
+
"name": pdf_file.stem,
|
427 |
+
"id": pdf_id,
|
428 |
+
"exists": True,
|
429 |
+
"cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
|
430 |
+
}
|
431 |
+
return {"exists": False, "error": "PDF를 찾을 수 없습니다"}
|
432 |
+
|
433 |
# API 엔드포인트: PDF 썸네일 제공 (최적화)
|
434 |
@app.get("/api/pdf-thumbnail")
|
435 |
async def get_pdf_thumbnail(path: str):
|
|
|
554 |
logger.error(f"PDF 콘텐츠 로드 오류: {str(e)}\n{error_details}")
|
555 |
return JSONResponse(content={"error": str(e)}, status_code=500)
|
556 |
|
|
|
557 |
# PDF 업로드 엔드포인트 - 영구 저장소에 저장 및 메인 화면에 자동 표시
|
558 |
@app.post("/api/upload-pdf")
|
559 |
async def upload_pdf(file: UploadFile = File(...)):
|
|
|
577 |
with open(PDF_DIR / file.filename, "wb") as buffer:
|
578 |
buffer.write(content)
|
579 |
|
580 |
+
# PDF ID 생성 및 메타데이터 저장
|
581 |
+
pdf_id = generate_pdf_id(file.filename)
|
582 |
+
pdf_metadata[pdf_id] = str(file_path)
|
583 |
+
save_pdf_metadata()
|
584 |
+
|
585 |
# 백그라운드에서 캐싱 시작
|
586 |
asyncio.create_task(cache_pdf(str(file_path)))
|
587 |
|
588 |
return JSONResponse(
|
589 |
+
content={
|
590 |
+
"success": True,
|
591 |
+
"path": str(file_path),
|
592 |
+
"name": file_path.stem,
|
593 |
+
"id": pdf_id,
|
594 |
+
"viewUrl": f"/view/{pdf_id}"
|
595 |
+
},
|
596 |
status_code=200
|
597 |
)
|
598 |
except Exception as e:
|
|
|
604 |
status_code=500
|
605 |
)
|
606 |
|
|
|
607 |
# 관리자 인증 엔드포인트
|
608 |
@app.post("/api/admin-login")
|
609 |
async def admin_login(password: str = Form(...)):
|
|
|
632 |
if pdf_name in pdf_cache:
|
633 |
del pdf_cache[pdf_name]
|
634 |
|
635 |
+
# 메타데이터에서 해당 파일 ID 제거
|
636 |
+
to_remove = []
|
637 |
+
for pid, fpath in pdf_metadata.items():
|
638 |
+
if os.path.basename(fpath) == pdf_file.name:
|
639 |
+
to_remove.append(pid)
|
640 |
+
|
641 |
+
for pid in to_remove:
|
642 |
+
del pdf_metadata[pid]
|
643 |
+
|
644 |
+
save_pdf_metadata()
|
645 |
+
|
646 |
return {"success": True}
|
647 |
except Exception as e:
|
648 |
logger.error(f"PDF 삭제 오류: {str(e)}")
|
|
|
680 |
logger.error(f"PDF 표시 해제 오류: {str(e)}")
|
681 |
return {"success": False, "message": str(e)}
|
682 |
|
683 |
+
# 직접 PDF 뷰어 URL 접근용 라우트
|
684 |
+
@app.get("/view/{pdf_id}")
|
685 |
+
async def view_pdf_by_id(pdf_id: str):
|
686 |
+
# PDF ID 유효한지 확인
|
687 |
+
pdf_path = get_pdf_path_by_id(pdf_id)
|
688 |
+
if not pdf_path:
|
689 |
+
return HTMLResponse(
|
690 |
+
content=f"<html><body><h1>PDF를 찾을 수 없습니다</h1><p>ID: {pdf_id}</p><a href='/'>홈으로 돌아가기</a></body></html>",
|
691 |
+
status_code=404
|
692 |
+
)
|
693 |
+
|
694 |
+
# 메인 페이지로 리다이렉트하되, PDF ID 파라미터 추가
|
695 |
+
return get_html_content(pdf_id=pdf_id)
|
696 |
+
|
697 |
# HTML 파일 읽기 함수
|
698 |
+
def get_html_content(pdf_id: str = None):
|
699 |
html_path = BASE / "flipbook_template.html"
|
700 |
+
content = ""
|
701 |
if html_path.exists():
|
702 |
with open(html_path, "r", encoding="utf-8") as f:
|
703 |
+
content = f.read()
|
704 |
+
else:
|
705 |
+
content = HTML # 기본 HTML 사용
|
706 |
+
|
707 |
+
# PDF ID가 제공된 경우, 자동 로드 스크립트 추가
|
708 |
+
if pdf_id:
|
709 |
+
auto_load_script = f"""
|
710 |
+
<script>
|
711 |
+
// 페이지 로드 시 자동으로 해당 PDF 열기
|
712 |
+
document.addEventListener('DOMContentLoaded', async function() {{
|
713 |
+
try {{
|
714 |
+
// PDF 정보 가져오기
|
715 |
+
const response = await fetch('/api/pdf-info-by-id/{pdf_id}');
|
716 |
+
const pdfInfo = await response.json();
|
717 |
+
|
718 |
+
if (pdfInfo.exists && pdfInfo.path) {{
|
719 |
+
// 약간의 지연 후 PDF 뷰어 열기 (UI가 준비된 후)
|
720 |
+
setTimeout(() => {{
|
721 |
+
openPdfById('{pdf_id}', pdfInfo.path, pdfInfo.cached);
|
722 |
+
}}, 500);
|
723 |
+
}} else {{
|
724 |
+
showError("요청한 PDF를 찾을 수 없습니다.");
|
725 |
+
}}
|
726 |
+
}} catch (e) {{
|
727 |
+
console.error("자동 PDF 로드 오류:", e);
|
728 |
+
}}
|
729 |
+
}});
|
730 |
+
</script>
|
731 |
+
"""
|
732 |
+
|
733 |
+
# body 종료 태그 전에 스크립트 삽입
|
734 |
+
content = content.replace("</body>", auto_load_script + "</body>")
|
735 |
+
|
736 |
+
return HTMLResponse(content=content)
|
737 |
+
|
738 |
+
@app.get("/", response_class=HTMLResponse)
|
739 |
+
async def root(request: Request, pdf_id: Optional[str] = Query(None)):
|
740 |
+
# PDF ID가 쿼리 파라미터로 제공된 경우 /view/{pdf_id}로 리다이렉트
|
741 |
+
if pdf_id:
|
742 |
+
return RedirectResponse(url=f"/view/{pdf_id}")
|
743 |
+
return get_html_content()
|
744 |
|
745 |
# HTML 문자열 (UI 수정 버전)
|
746 |
HTML = """
|
|
|
1504 |
// 현재 페이지 로딩 상태
|
1505 |
let currentLoadingPdfPath = null;
|
1506 |
let pageLoadingInterval = null;
|
1507 |
+
|
1508 |
+
// 현재 열린 PDF의 ID
|
1509 |
+
let currentPdfId = null;
|
1510 |
|
1511 |
/* 🔊 오디오 unlock – 내장 Audio 와 같은 MP3 경로 사용 */
|
1512 |
['click','touchstart'].forEach(evt=>{
|
|
|
1580 |
await loadServerPDFs();
|
1581 |
|
1582 |
// 성공 메시지
|
1583 |
+
showMessage("PDF가 성공적으로 업로드되었습니다! 공유 URL: " + result.viewUrl);
|
1584 |
} else {
|
1585 |
hideLoading();
|
1586 |
showError("업로드 실패: " + (result.message || "알 수 없는 오류"));
|
|
|
1592 |
}
|
1593 |
}
|
1594 |
|
1595 |
+
function addCard(i, thumb, title, isCached = false, pdfId = null) {
|
1596 |
const d = document.createElement('div');
|
1597 |
d.className = 'card fade-in';
|
1598 |
d.onclick = () => open(i);
|
1599 |
|
1600 |
+
// PDF ID가 있으면 데이터 속성으로 저장
|
1601 |
+
if (pdfId) {
|
1602 |
+
d.dataset.pdfId = pdfId;
|
1603 |
+
}
|
1604 |
+
|
1605 |
// 제목 처리
|
1606 |
const displayTitle = title ?
|
1607 |
+
(title.length > 15 ? title.substring(0, 15) + '...' : title) :
|
1608 |
+
'프로젝트 ' + (i+1);
|
1609 |
|
1610 |
// 캐시 상태 뱃지 추가
|
1611 |
const cachedBadge = isCached ?
|
1612 |
+
'<div class="cached-status">캐시됨</div>' : '';
|
1613 |
|
1614 |
d.innerHTML = `
|
1615 |
+
<div class="card-inner">
|
1616 |
+
${cachedBadge}
|
1617 |
+
<img src="${thumb}" alt="${displayTitle}" loading="lazy">
|
1618 |
+
<p title="${title || '프로젝트 ' + (i+1)}">${displayTitle}</p>
|
1619 |
+
</div>
|
1620 |
`;
|
1621 |
grid.appendChild(d);
|
1622 |
|
|
|
1625 |
}
|
1626 |
|
1627 |
/* ── 프로젝트 저장 ── */
|
1628 |
+
function save(pages, title, isCached = false, pdfId = null) {
|
1629 |
+
const id = projects.push(pages) - 1;
|
1630 |
+
addCard(id, pages[0].thumb, title, isCached, pdfId);
|
1631 |
}
|
1632 |
|
1633 |
/* ── 서버 PDF 로드 및 캐시 상태 확인 ── */
|
|
|
1679 |
return {
|
1680 |
pages,
|
1681 |
name: project.name,
|
1682 |
+
isCached,
|
1683 |
+
id: project.id
|
1684 |
};
|
1685 |
}
|
1686 |
} catch (err) {
|
|
|
1695 |
|
1696 |
// 성공적으로 가져온 결과만 표시
|
1697 |
results.filter(result => result !== null).forEach(result => {
|
1698 |
+
save(result.pages, result.name, result.isCached, result.id);
|
1699 |
});
|
1700 |
|
1701 |
hideLoading();
|
|
|
1780 |
}
|
1781 |
}
|
1782 |
|
1783 |
+
/* ── PDF ID로 PDF 열기 ── */
|
1784 |
+
async function openPdfById(pdfId, pdfPath, isCached = false) {
|
1785 |
+
try {
|
1786 |
+
// 먼저 홈 화면에서 카드를 찾아서 클릭하는 방법 시도
|
1787 |
+
let foundCard = false;
|
1788 |
+
const cards = document.querySelectorAll('.card');
|
1789 |
+
|
1790 |
+
for (let i = 0; i < cards.length; i++) {
|
1791 |
+
if (cards[i].dataset.pdfId === pdfId) {
|
1792 |
+
cards[i].click();
|
1793 |
+
foundCard = true;
|
1794 |
+
break;
|
1795 |
+
}
|
1796 |
+
}
|
1797 |
+
|
1798 |
+
// 카드를 찾지 못한 경우 직접 오픈
|
1799 |
+
if (!foundCard) {
|
1800 |
+
toggle(false);
|
1801 |
+
showLoading("PDF 준비 중...");
|
1802 |
+
|
1803 |
+
let pages = [];
|
1804 |
+
|
1805 |
+
// 이미 캐시된 경우 캐시된 데이터 사용
|
1806 |
+
if (isCached) {
|
1807 |
+
try {
|
1808 |
+
const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
|
1809 |
+
const cachedData = await response.json();
|
1810 |
+
|
1811 |
+
if (cachedData.status === "completed" && cachedData.pages) {
|
1812 |
+
hideLoading();
|
1813 |
+
createFlipBook(cachedData.pages);
|
1814 |
+
// 현재 열린 PDF의 ID 저장
|
1815 |
+
currentPdfId = pdfId;
|
1816 |
+
return;
|
1817 |
+
}
|
1818 |
+
} catch (error) {
|
1819 |
+
console.error("캐시 데이터 로드 실패:", error);
|
1820 |
+
}
|
1821 |
+
}
|
1822 |
+
|
1823 |
+
// 썸네일 가져오기
|
1824 |
+
try {
|
1825 |
+
const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdfPath)}`);
|
1826 |
+
const thumbData = await thumbResponse.json();
|
1827 |
+
|
1828 |
+
if (thumbData.thumbnail) {
|
1829 |
+
pages = [{
|
1830 |
+
src: thumbData.thumbnail,
|
1831 |
+
thumb: thumbData.thumbnail,
|
1832 |
+
path: pdfPath,
|
1833 |
+
cached: isCached
|
1834 |
+
}];
|
1835 |
+
}
|
1836 |
+
} catch (error) {
|
1837 |
+
console.error("썸네일 로드 실패:", error);
|
1838 |
+
}
|
1839 |
+
|
1840 |
+
// 일단 기본 페이지 추가
|
1841 |
+
if (pages.length === 0) {
|
1842 |
+
pages = [{
|
1843 |
+
path: pdfPath,
|
1844 |
+
cached: isCached
|
1845 |
+
}];
|
1846 |
+
}
|
1847 |
+
|
1848 |
+
// 프로젝트에 추가하고 뷰어 실행
|
1849 |
+
const projectId = projects.push(pages) - 1;
|
1850 |
+
hideLoading();
|
1851 |
+
open(projectId);
|
1852 |
+
|
1853 |
+
// 현재 열린 PDF의 ID 저장
|
1854 |
+
currentPdfId = pdfId;
|
1855 |
+
}
|
1856 |
+
} catch (error) {
|
1857 |
+
console.error("PDF ID로 열기 실패:", error);
|
1858 |
+
hideLoading();
|
1859 |
+
showError("PDF를 열 수 없습니다. 다시 시도해주세요.");
|
1860 |
+
}
|
1861 |
+
}
|
1862 |
+
|
1863 |
+
/* ── 현재 PDF의 고유 URL 생성 및 복사 ── */
|
1864 |
+
function copyPdfShareUrl() {
|
1865 |
+
if (!currentPdfId) {
|
1866 |
+
showError("공유할 PDF가 없습니다.");
|
1867 |
+
return;
|
1868 |
+
}
|
1869 |
+
|
1870 |
+
// 현재 도메인 기반 전체 URL 생성
|
1871 |
+
const shareUrl = `${window.location.origin}/view/${currentPdfId}`;
|
1872 |
+
|
1873 |
+
// 클립보드에 복사
|
1874 |
+
navigator.clipboard.writeText(shareUrl)
|
1875 |
+
.then(() => {
|
1876 |
+
showMessage("PDF 링크가 복사되었습니다!");
|
1877 |
+
})
|
1878 |
+
.catch(err => {
|
1879 |
+
console.error("클립보드 복사 실패:", err);
|
1880 |
+
showError("링크 복사에 실패했습니다.");
|
1881 |
+
});
|
1882 |
+
}
|
1883 |
+
|
1884 |
/* ── 카드 → FlipBook ── */
|
1885 |
async function open(i) {
|
1886 |
toggle(false);
|
1887 |
const pages = projects[i];
|
1888 |
|
1889 |
+
// PDF ID 찾기 및 저장
|
1890 |
+
const card = document.querySelectorAll('.card')[i];
|
1891 |
+
if (card && card.dataset.pdfId) {
|
1892 |
+
currentPdfId = card.dataset.pdfId;
|
1893 |
+
} else {
|
1894 |
+
currentPdfId = null;
|
1895 |
+
}
|
1896 |
+
|
1897 |
// 기존 FlipBook 정리
|
1898 |
if(fb) {
|
1899 |
fb.destroy();
|
|
|
2157 |
enableDownload: false,
|
2158 |
enablePrint: false,
|
2159 |
enableZoom: true,
|
2160 |
+
enableShare: true, // 공유 버튼 활성화
|
2161 |
enableSearch: true,
|
2162 |
enableAutoPlay: true,
|
2163 |
enableAnnotation: false,
|
|
|
2174 |
pageTextureSize: 1024, // 페이지 텍스처 크기
|
2175 |
thumbnails: true, // 섬네일 활성화
|
2176 |
autoHideControls: false, // 자동 숨김 비활성화
|
2177 |
+
controlsTimeout: 8000, // 컨트롤 표시 시간 연장
|
2178 |
+
shareHandler: copyPdfShareUrl // 공유 핸들러 설정
|
2179 |
}
|
2180 |
});
|
2181 |
|
|
|
2230 |
}
|
2231 |
$id('loadingPages').style.display = 'none';
|
2232 |
currentLoadingPdfPath = null;
|
2233 |
+
currentPdfId = null;
|
2234 |
};
|
2235 |
|
2236 |
function toggle(showHome){
|
|
|
2345 |
const card = document.createElement('div');
|
2346 |
card.className = 'admin-card card fade-in';
|
2347 |
|
2348 |
+
// 고유 URL 생성
|
2349 |
+
const viewUrl = `${window.location.origin}/view/${pdf.id}`;
|
2350 |
+
|
2351 |
// 썸네일 및 정보
|
2352 |
card.innerHTML = `
|
2353 |
<div class="card-inner">
|
2354 |
${pdf.cached ? '<div class="cached-status">캐시됨</div>' : ''}
|
2355 |
<img src="${thumbData.thumbnail || ''}" alt="${pdf.name}" loading="lazy">
|
2356 |
<p title="${pdf.name}">${pdf.name.length > 15 ? pdf.name.substring(0, 15) + '...' : pdf.name}</p>
|
2357 |
+
<div style="position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%); z-index:10;">
|
2358 |
+
<a href="${viewUrl}" target="_blank" style="color:#4a6ee0; font-size:12px;">바로가기 링크</a>
|
2359 |
+
</div>
|
2360 |
${isMainDisplayed ?
|
2361 |
`<button class="unfeature-btn" data-path="${pdf.path}">메인에서 제거</button>` :
|
2362 |
`<button class="feature-btn" data-path="${pdf.path}">메인에 표시</button>`}
|
|
|
2611 |
</html>
|
2612 |
"""
|
2613 |
|
|
|
|
|
|
|
|
|
2614 |
if __name__ == "__main__":
|
2615 |
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
|