ginipick commited on
Commit
285f7e3
·
verified ·
1 Parent(s): 3486e32

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +361 -33
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={"success": True, "path": str(file_path), "name": file_path.stem},
 
 
 
 
 
 
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
- return f.read()
537
- return HTML # 기본 HTML 사용
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- (title.length > 15 ? title.substring(0, 15) + '...' : title) :
1394
- '프로젝트 ' + (i+1);
1395
 
1396
  // 캐시 상태 뱃지 추가
1397
  const cachedBadge = isCached ?
1398
- '<div class="cached-status">캐시됨</div>' : '';
1399
 
1400
  d.innerHTML = `
1401
- <div class="card-inner">
1402
- ${cachedBadge}
1403
- <img src="${thumb}" alt="${displayTitle}" loading="lazy">
1404
- <p title="${title || '프로젝트 ' + (i+1)}">${displayTitle}</p>
1405
- </div>
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: false,
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)))