ginipick commited on
Commit
e7b2ed2
ยท
1 Parent(s): 58df079

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +464 -51
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,46 @@ 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 +99,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 +120,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 +300,74 @@ 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 +375,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:
@@ -275,6 +416,28 @@ async def init_cache_all_pdfs():
275
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹œ์ž‘ ํ•จ์ˆ˜
276
  @app.on_event("startup")
277
  async def startup_event():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒœ์Šคํฌ๋กœ ์บ์‹ฑ ์‹คํ–‰
279
  asyncio.create_task(init_cache_all_pdfs())
280
 
@@ -290,14 +453,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 +614,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 +637,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 +664,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 +692,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 +740,83 @@ 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 +1580,9 @@ HTML = """
1298
  // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ
1299
  let currentLoadingPdfPath = null;
1300
  let pageLoadingInterval = null;
 
 
 
1301
 
1302
  /* ๐Ÿ”Š ์˜ค๋””์˜ค unlock โ€“ ๋‚ด์žฅ Audio ์™€ ๊ฐ™์€ MP3 ๊ฒฝ๋กœ ์‚ฌ์šฉ */
1303
  ['click','touchstart'].forEach(evt=>{
@@ -1371,7 +1656,7 @@ HTML = """
1371
  await loadServerPDFs();
1372
 
1373
  // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
1374
- showMessage("PDF๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
1375
  } else {
1376
  hideLoading();
1377
  showError("์—…๋กœ๋“œ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
@@ -1382,38 +1667,52 @@ HTML = """
1382
  showError("PDF ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
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
-
1409
- // ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์œผ๋ฉด 'ํ”„๋กœ์ ํŠธ ์—†์Œ' ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
1410
- $id('noProjects').style.display = 'none';
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 +1764,8 @@ HTML = """
1465
  return {
1466
  pages,
1467
  name: project.name,
1468
- isCached
 
1469
  };
1470
  }
1471
  } catch (err) {
@@ -1480,7 +1780,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 +1865,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 +2242,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 +2259,8 @@ HTML = """
1850
  pageTextureSize: 1024, // ํŽ˜์ด์ง€ ํ…์Šค์ฒ˜ ํฌ๊ธฐ
1851
  thumbnails: true, // ์„ฌ๋„ค์ผ ํ™œ์„ฑํ™”
1852
  autoHideControls: false, // ์ž๋™ ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
1853
- controlsTimeout: 8000 // ์ปจํŠธ๋กค ํ‘œ์‹œ ์‹œ๊ฐ„ ์—ฐ์žฅ
 
1854
  }
1855
  });
1856
 
@@ -1905,6 +2315,7 @@ HTML = """
1905
  }
1906
  $id('loadingPages').style.display = 'none';
1907
  currentLoadingPdfPath = null;
 
1908
  };
1909
 
1910
  function toggle(showHome){
@@ -2019,12 +2430,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 +2696,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
+ # PDF ID ์ƒ์„ฑ (ํŒŒ์ผ๋ช… + ํƒ€์ž„์Šคํƒฌํ”„ ๊ธฐ๋ฐ˜) - ๋” ๋‹จ์ˆœํ•˜๊ณ  ์•ˆ์ „ํ•œ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝ
74
+ def generate_pdf_id(filename: str) -> str:
75
+ # ํŒŒ์ผ๋ช…์—์„œ ํ™•์žฅ์ž ์ œ๊ฑฐ
76
+ base_name = os.path.splitext(filename)[0]
77
+ # ์•ˆ์ „ํ•œ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (URL ์ธ์ฝ”๋”ฉ ๋Œ€์‹  ์ง์ ‘ ๋ณ€ํ™˜)
78
+ import re
79
+ safe_name = re.sub(r'[^\w\-_]', '_', base_name.replace(" ", "_"))
80
+ # ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€๋กœ ๊ณ ์œ ์„ฑ ๋ณด์žฅ
81
+ timestamp = int(time.time())
82
+ # ์งง์€ ์ž„์˜ ๋ฌธ์ž์—ด ์ถ”๊ฐ€
83
+ random_suffix = uuid.uuid4().hex[:6]
84
+ return f"{safe_name}_{timestamp}_{random_suffix}"
85
+
86
+
87
 
88
  # PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ (๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์šฉ)
89
  def get_pdf_files():
 
99
  pdf_files = [f for f in PERMANENT_PDF_DIR.glob("*.pdf")]
100
  return pdf_files
101
 
 
102
  # PDF ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
103
  def generate_pdf_projects():
104
  projects_data = []
 
120
 
121
  # ์ค‘๋ณต ์ œ๊ฑฐ๋œ ํŒŒ์ผ๋“ค๋กœ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
122
  for pdf_file in unique_files.values():
123
+ # ํ•ด๋‹น ํŒŒ์ผ์˜ PDF ID ์ฐพ๊ธฐ
124
+ pdf_id = None
125
+ for pid, path in pdf_metadata.items():
126
+ if os.path.basename(path) == pdf_file.name:
127
+ pdf_id = pid
128
+ break
129
+
130
+ # ID๊ฐ€ ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑํ•˜๊ณ  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€
131
+ if not pdf_id:
132
+ pdf_id = generate_pdf_id(pdf_file.name)
133
+ pdf_metadata[pdf_id] = str(pdf_file)
134
+ save_pdf_metadata()
135
+
136
  projects_data.append({
137
  "path": str(pdf_file),
138
  "name": pdf_file.stem,
139
+ "id": pdf_id,
140
  "cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
141
  })
142
 
143
  return projects_data
144
 
 
145
  # ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ ์ƒ์„ฑ
146
  def get_cache_path(pdf_name: str):
147
  return CACHE_DIR / f"{pdf_name}_cache.json"
 
300
  pdf_cache[pdf_name]["status"] = "error"
301
  pdf_cache[pdf_name]["error"] = str(e)
302
 
303
+ # PDF ID๋กœ PDF ๊ฒฝ๋กœ ์ฐพ๊ธฐ
304
+ # PDF ID๋กœ PDF ๊ฒฝ๋กœ ์ฐพ๊ธฐ (๊ฐœ์„ ๋œ ๊ฒ€์ƒ‰ ๋กœ์ง)
305
+ def get_pdf_path_by_id(pdf_id: str) -> str:
306
+ logger.info(f"PDF ID๋กœ ํŒŒ์ผ ์กฐํšŒ: {pdf_id}")
307
+
308
+ # 1. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ์ง์ ‘ ID๋กœ ๊ฒ€์ƒ‰
309
+ if pdf_id in pdf_metadata:
310
+ path = pdf_metadata[pdf_id]
311
+ # ํŒŒ์ผ ์กด์žฌ ํ™•์ธ
312
+ if os.path.exists(path):
313
+ return path
314
+
315
+ # ํŒŒ์ผ์ด ์ด๋™ํ–ˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํŒŒ์ผ๋ช…์œผ๋กœ ๊ฒ€์ƒ‰
316
+ filename = os.path.basename(path)
317
+
318
+ # ์˜๊ตฌ ์ €์žฅ์†Œ์—์„œ ๊ฒ€์ƒ‰
319
+ perm_path = PERMANENT_PDF_DIR / filename
320
+ if perm_path.exists():
321
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
322
+ pdf_metadata[pdf_id] = str(perm_path)
323
+ save_pdf_metadata()
324
+ return str(perm_path)
325
+
326
+ # ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ๊ฒ€์ƒ‰
327
+ main_path = PDF_DIR / filename
328
+ if main_path.exists():
329
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
330
+ pdf_metadata[pdf_id] = str(main_path)
331
+ save_pdf_metadata()
332
+ return str(main_path)
333
+
334
+ # 2. ํŒŒ์ผ๋ช… ๋ถ€๋ถ„๋งŒ ์ถ”์ถœํ•˜์—ฌ ๋ชจ๋“  PDF ํŒŒ์ผ ๊ฒ€์ƒ‰
335
+ try:
336
+ # ID ํ˜•์‹: filename_timestamp_random
337
+ # ํŒŒ์ผ๋ช… ๋ถ€๋ถ„๋งŒ ์ถ”์ถœ
338
+ name_part = pdf_id.split('_')[0] if '_' in pdf_id else pdf_id
339
+
340
+ # ๋ชจ๋“  PDF ํŒŒ์ผ ๊ฒ€์ƒ‰
341
+ for file_path in get_pdf_files() + get_permanent_pdf_files():
342
+ # ํŒŒ์ผ๋ช…์ด ID์˜ ์‹œ์ž‘ ๋ถ€๋ถ„๊ณผ ์ผ์น˜ํ•˜๋ฉด
343
+ file_basename = os.path.basename(file_path)
344
+ if file_basename.startswith(name_part) or file_path.stem.startswith(name_part):
345
+ # ID ๋งคํ•‘ ์—…๋ฐ์ดํŠธ
346
+ pdf_metadata[pdf_id] = str(file_path)
347
+ save_pdf_metadata()
348
+ return str(file_path)
349
+ except Exception as e:
350
+ logger.error(f"ํŒŒ์ผ๋ช… ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜: {e}")
351
+
352
+ # 3. ๋ชจ๋“  PDF ํŒŒ์ผ์— ๋Œ€ํ•ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ™•์ธ
353
+ for pid, path in pdf_metadata.items():
354
+ if os.path.exists(path):
355
+ file_basename = os.path.basename(path)
356
+ # ์œ ์‚ฌํ•œ ํŒŒ์ผ๋ช…์„ ๊ฐ€์ง„ ๊ฒฝ์šฐ
357
+ if pdf_id in pid or pid in pdf_id:
358
+ pdf_metadata[pdf_id] = path
359
+ save_pdf_metadata()
360
+ return path
361
+
362
+ return None
363
+
364
  # ์‹œ์ž‘ ์‹œ ๋ชจ๋“  PDF ํŒŒ์ผ ์บ์‹ฑ
365
  async def init_cache_all_pdfs():
366
  logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
367
 
368
+ # PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
369
+ load_pdf_metadata()
370
+
371
  # ๋ฉ”์ธ ๋ฐ ์˜๊ตฌ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ PDF ํŒŒ์ผ ๋ชจ๋‘ ๊ฐ€์ ธ์˜ค๊ธฐ
372
  pdf_files = get_pdf_files() + get_permanent_pdf_files()
373
 
 
375
  unique_pdf_paths = set(str(p) for p in pdf_files)
376
  pdf_files = [pathlib.Path(p) for p in unique_pdf_paths]
377
 
378
+ # ํŒŒ์ผ ๊ธฐ๋ฐ˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
379
+ for pdf_file in pdf_files:
380
+ # ID๊ฐ€ ์—†๋Š” ํŒŒ์ผ์— ๋Œ€ํ•ด ID ์ƒ์„ฑ
381
+ found = False
382
+ for pid, path in pdf_metadata.items():
383
+ if os.path.basename(path) == pdf_file.name:
384
+ found = True
385
+ # ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
386
+ if not os.path.exists(path):
387
+ pdf_metadata[pid] = str(pdf_file)
388
+ break
389
+
390
+ if not found:
391
+ pdf_id = generate_pdf_id(pdf_file.name)
392
+ pdf_metadata[pdf_id] = str(pdf_file)
393
+
394
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
395
+ save_pdf_metadata()
396
+
397
  # ์ด๋ฏธ ์บ์‹œ๋œ PDF ํŒŒ์ผ ๋กœ๋“œ (๋น ๋ฅธ ์‹œ์ž‘์„ ์œ„ํ•ด ๋จผ์ € ์ˆ˜ํ–‰)
398
  for cache_file in CACHE_DIR.glob("*_cache.json"):
399
  try:
 
416
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹œ์ž‘ ํ•จ์ˆ˜
417
  @app.on_event("startup")
418
  async def startup_event():
419
+ # PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
420
+ load_pdf_metadata()
421
+
422
+ # ๋ˆ„๋ฝ๋œ PDF ํŒŒ์ผ์— ๋Œ€ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ
423
+ for pdf_file in get_pdf_files() + get_permanent_pdf_files():
424
+ found = False
425
+ for pid, path in pdf_metadata.items():
426
+ if os.path.basename(path) == pdf_file.name:
427
+ found = True
428
+ # ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ
429
+ if not os.path.exists(path):
430
+ pdf_metadata[pid] = str(pdf_file)
431
+ break
432
+
433
+ if not found:
434
+ # ์ƒˆ ID ์ƒ์„ฑ ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€
435
+ pdf_id = generate_pdf_id(pdf_file.name)
436
+ pdf_metadata[pdf_id] = str(pdf_file)
437
+
438
+ # ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ
439
+ save_pdf_metadata()
440
+
441
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒœ์Šคํฌ๋กœ ์บ์‹ฑ ์‹คํ–‰
442
  asyncio.create_task(init_cache_all_pdfs())
443
 
 
453
  projects_data = []
454
 
455
  for pdf_file in pdf_files:
456
+ # PDF ID ์ฐพ๊ธฐ
457
+ pdf_id = None
458
+ for pid, path in pdf_metadata.items():
459
+ if os.path.basename(path) == pdf_file.name:
460
+ pdf_id = pid
461
+ break
462
+
463
+ # ID๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
464
+ if not pdf_id:
465
+ pdf_id = generate_pdf_id(pdf_file.name)
466
+ pdf_metadata[pdf_id] = str(pdf_file)
467
+ save_pdf_metadata()
468
+
469
  projects_data.append({
470
  "path": str(pdf_file),
471
  "name": pdf_file.stem,
472
+ "id": pdf_id,
473
  "cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
474
  })
475
 
476
  return projects_data
477
 
478
+ # API ์—”๋“œํฌ์ธํŠธ: PDF ID๋กœ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
479
+ @app.get("/api/pdf-info-by-id/{pdf_id}")
480
+ async def get_pdf_info_by_id(pdf_id: str):
481
+ pdf_path = get_pdf_path_by_id(pdf_id)
482
+ if pdf_path:
483
+ pdf_file = pathlib.Path(pdf_path)
484
+ return {
485
+ "path": pdf_path,
486
+ "name": pdf_file.stem,
487
+ "id": pdf_id,
488
+ "exists": True,
489
+ "cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
490
+ }
491
+ return {"exists": False, "error": "PDF๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}
492
+
493
  # API ์—”๋“œํฌ์ธํŠธ: PDF ์ธ๋„ค์ผ ์ œ๊ณต (์ตœ์ ํ™”)
494
  @app.get("/api/pdf-thumbnail")
495
  async def get_pdf_thumbnail(path: str):
 
614
  logger.error(f"PDF ์ฝ˜ํ…์ธ  ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
615
  return JSONResponse(content={"error": str(e)}, status_code=500)
616
 
 
617
  # PDF ์—…๋กœ๋“œ ์—”๋“œํฌ์ธํŠธ - ์˜๊ตฌ ์ €์žฅ์†Œ์— ์ €์žฅ ๋ฐ ๋ฉ”์ธ ํ™”๋ฉด์— ์ž๋™ ํ‘œ์‹œ
618
  @app.post("/api/upload-pdf")
619
  async def upload_pdf(file: UploadFile = File(...)):
 
637
  with open(PDF_DIR / file.filename, "wb") as buffer:
638
  buffer.write(content)
639
 
640
+ # PDF ID ์ƒ์„ฑ ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
641
+ pdf_id = generate_pdf_id(file.filename)
642
+ pdf_metadata[pdf_id] = str(file_path)
643
+ save_pdf_metadata()
644
+
645
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
646
  asyncio.create_task(cache_pdf(str(file_path)))
647
 
648
  return JSONResponse(
649
+ content={
650
+ "success": True,
651
+ "path": str(file_path),
652
+ "name": file_path.stem,
653
+ "id": pdf_id,
654
+ "viewUrl": f"/view/{pdf_id}"
655
+ },
656
  status_code=200
657
  )
658
  except Exception as e:
 
664
  status_code=500
665
  )
666
 
 
667
  # ๊ด€๋ฆฌ์ž ์ธ์ฆ ์—”๋“œํฌ์ธํŠธ
668
  @app.post("/api/admin-login")
669
  async def admin_login(password: str = Form(...)):
 
692
  if pdf_name in pdf_cache:
693
  del pdf_cache[pdf_name]
694
 
695
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ํŒŒ์ผ ID ์ œ๊ฑฐ
696
+ to_remove = []
697
+ for pid, fpath in pdf_metadata.items():
698
+ if os.path.basename(fpath) == pdf_file.name:
699
+ to_remove.append(pid)
700
+
701
+ for pid in to_remove:
702
+ del pdf_metadata[pid]
703
+
704
+ save_pdf_metadata()
705
+
706
  return {"success": True}
707
  except Exception as e:
708
  logger.error(f"PDF ์‚ญ์ œ ์˜ค๋ฅ˜: {str(e)}")
 
740
  logger.error(f"PDF ํ‘œ์‹œ ํ•ด์ œ ์˜ค๋ฅ˜: {str(e)}")
741
  return {"success": False, "message": str(e)}
742
 
743
+ # ์ง์ ‘ PDF ๋ทฐ์–ด URL ์ ‘๊ทผ์šฉ ๋ผ์šฐํŠธ
744
+ @app.get("/view/{pdf_id}")
745
+ async def view_pdf_by_id(pdf_id: str):
746
+ # PDF ID ์œ ํšจํ•œ์ง€ ํ™•์ธ
747
+ pdf_path = get_pdf_path_by_id(pdf_id)
748
+
749
+ if not pdf_path:
750
+ # ์ผ๋‹จ ๋ชจ๋“  PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•˜๊ณ  ์žฌ์‹œ๋„
751
+ load_pdf_metadata()
752
+ pdf_path = get_pdf_path_by_id(pdf_id)
753
+
754
+ if not pdf_path:
755
+ # ๋ชจ๋“  PDF ํŒŒ์ผ์„ ์ง์ ‘ ์Šค์บ”ํ•˜์—ฌ ์œ ์‚ฌํ•œ ์ด๋ฆ„ ์ฐพ๊ธฐ
756
+ for file_path in get_pdf_files() + get_permanent_pdf_files():
757
+ name_part = pdf_id.split('_')[0] if '_' in pdf_id else pdf_id
758
+ if file_path.stem.startswith(name_part):
759
+ pdf_metadata[pdf_id] = str(file_path)
760
+ save_pdf_metadata()
761
+ pdf_path = str(file_path)
762
+ break
763
+
764
+ if not pdf_path:
765
+ return HTMLResponse(
766
+ content=f"<html><body><h1>PDF๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</h1><p>ID: {pdf_id}</p><a href='/'>ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a></body></html>",
767
+ status_code=404
768
+ )
769
+
770
+ # ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๋˜, PDF ID ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
771
+ return get_html_content(pdf_id=pdf_id)
772
+
773
  # HTML ํŒŒ์ผ ์ฝ๊ธฐ ํ•จ์ˆ˜
774
+ def get_html_content(pdf_id: str = None):
775
  html_path = BASE / "flipbook_template.html"
776
+ content = ""
777
  if html_path.exists():
778
  with open(html_path, "r", encoding="utf-8") as f:
779
+ content = f.read()
780
+ else:
781
+ content = HTML # ๊ธฐ๋ณธ HTML ์‚ฌ์šฉ
782
+
783
+ # PDF ID๊ฐ€ ์ œ๊ณต๋œ ๊ฒฝ์šฐ, ์ž๋™ ๋กœ๋“œ ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€
784
+ if pdf_id:
785
+ auto_load_script = f"""
786
+ <script>
787
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ž๋™์œผ๋กœ ํ•ด๋‹น PDF ์—ด๊ธฐ
788
+ document.addEventListener('DOMContentLoaded', async function() {{
789
+ try {{
790
+ // PDF ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
791
+ const response = await fetch('/api/pdf-info-by-id/{pdf_id}');
792
+ const pdfInfo = await response.json();
793
+
794
+ if (pdfInfo.exists && pdfInfo.path) {{
795
+ // ์•ฝ๊ฐ„์˜ ์ง€์—ฐ ํ›„ PDF ๋ทฐ์–ด ์—ด๊ธฐ (UI๊ฐ€ ์ค€๋น„๋œ ํ›„)
796
+ setTimeout(() => {{
797
+ openPdfById('{pdf_id}', pdfInfo.path, pdfInfo.cached);
798
+ }}, 500);
799
+ }} else {{
800
+ showError("์š”์ฒญํ•œ PDF๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
801
+ }}
802
+ }} catch (e) {{
803
+ console.error("์ž๋™ PDF ๋กœ๋“œ ์˜ค๋ฅ˜:", e);
804
+ }}
805
+ }});
806
+ </script>
807
+ """
808
+
809
+ # body ์ข…๋ฃŒ ํƒœ๊ทธ ์ „์— ์Šคํฌ๋ฆฝํŠธ ์‚ฝ์ž…
810
+ content = content.replace("</body>", auto_load_script + "</body>")
811
+
812
+ return HTMLResponse(content=content)
813
+
814
+ @app.get("/", response_class=HTMLResponse)
815
+ async def root(request: Request, pdf_id: Optional[str] = Query(None)):
816
+ # PDF ID๊ฐ€ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ œ๊ณต๋œ ๊ฒฝ์šฐ /view/{pdf_id}๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
817
+ if pdf_id:
818
+ return RedirectResponse(url=f"/view/{pdf_id}")
819
+ return get_html_content()
820
 
821
  # HTML ๋ฌธ์ž์—ด (UI ์ˆ˜์ • ๋ฒ„์ „)
822
  HTML = """
 
1580
  // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ
1581
  let currentLoadingPdfPath = null;
1582
  let pageLoadingInterval = null;
1583
+
1584
+ // ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID
1585
+ let currentPdfId = null;
1586
 
1587
  /* ๐Ÿ”Š ์˜ค๋””์˜ค unlock โ€“ ๋‚ด์žฅ Audio ์™€ ๊ฐ™์€ MP3 ๊ฒฝ๋กœ ์‚ฌ์šฉ */
1588
  ['click','touchstart'].forEach(evt=>{
 
1656
  await loadServerPDFs();
1657
 
1658
  // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
1659
+ showMessage("PDF๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๊ณต์œ  URL: " + result.viewUrl);
1660
  } else {
1661
  hideLoading();
1662
  showError("์—…๋กœ๋“œ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
 
1667
  showError("PDF ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1668
  }
1669
  }
1670
+
1671
+ function addCard(i, thumb, title, isCached = false, pdfId = null) {
1672
+ const d = document.createElement('div');
1673
+ d.className = 'card fade-in';
1674
+ d.onclick = () => open(i);
1675
+
1676
+ // PDF ID๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ์ดํ„ฐ ์†์„ฑ์œผ๋กœ ์ €์žฅ
1677
+ if (pdfId) {
1678
+ d.dataset.pdfId = pdfId;
1679
+ }
1680
+
1681
+ // ์ œ๋ชฉ ์ฒ˜๋ฆฌ
1682
+ const displayTitle = title ?
1683
+ (title.length > 15 ? title.substring(0, 15) + '...' : title) :
1684
+ 'ํ”„๋กœ์ ํŠธ ' + (i+1);
1685
+
1686
+ // ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์ถ”๊ฐ€
1687
+ const cachedBadge = isCached ?
1688
+ '<div class="cached-status">์บ์‹œ๋จ</div>' : '';
1689
+
1690
+ // ๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ ์ถ”๊ฐ€ (PDF ID๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ)
1691
+ const linkHtml = pdfId ?
1692
+ `<div style="position: absolute; bottom: 55px; left: 50%; transform: translateX(-50%); z-index:5;">
1693
+ <a href="/view/${pdfId}" target="_blank" style="color:#4a6ee0; font-size:11px;">Shaer Link</a>
1694
+ </div>` : '';
1695
+
1696
+ d.innerHTML = `
1697
+ <div class="card-inner">
1698
+ ${cachedBadge}
1699
+ <img src="${thumb}" alt="${displayTitle}" loading="lazy">
1700
+ ${linkHtml}
1701
+ <p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
1702
+ </div>
1703
+ `;
1704
+ grid.appendChild(d);
1705
+
1706
+ // ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์œผ๋ฉด 'ํ”„๋กœ์ ํŠธ ์—†์Œ' ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
1707
+ $id('noProjects').style.display = 'none';
1708
+ }
1709
+
1710
+
1711
 
1712
  /* โ”€โ”€ ํ”„๋กœ์ ํŠธ ์ €์žฅ โ”€โ”€ */
1713
+ function save(pages, title, isCached = false, pdfId = null) {
1714
+ const id = projects.push(pages) - 1;
1715
+ addCard(id, pages[0].thumb, title, isCached, pdfId);
1716
  }
1717
 
1718
  /* โ”€โ”€ ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ โ”€โ”€ */
 
1764
  return {
1765
  pages,
1766
  name: project.name,
1767
+ isCached,
1768
+ id: project.id
1769
  };
1770
  }
1771
  } catch (err) {
 
1780
 
1781
  // ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ ๊ฒฐ๊ณผ๋งŒ ํ‘œ์‹œ
1782
  results.filter(result => result !== null).forEach(result => {
1783
+ save(result.pages, result.name, result.isCached, result.id);
1784
  });
1785
 
1786
  hideLoading();
 
1865
  }
1866
  }
1867
 
1868
+ /* โ”€โ”€ PDF ID๋กœ PDF ์—ด๊ธฐ โ”€โ”€ */
1869
+ async function openPdfById(pdfId, pdfPath, isCached = false) {
1870
+ try {
1871
+ // ๋จผ์ € ํ™ˆ ํ™”๋ฉด์—์„œ ์นด๋“œ๋ฅผ ์ฐพ์•„์„œ ํด๋ฆญํ•˜๋Š” ๋ฐฉ๋ฒ• ์‹œ๋„
1872
+ let foundCard = false;
1873
+ const cards = document.querySelectorAll('.card');
1874
+
1875
+ for (let i = 0; i < cards.length; i++) {
1876
+ if (cards[i].dataset.pdfId === pdfId) {
1877
+ cards[i].click();
1878
+ foundCard = true;
1879
+ break;
1880
+ }
1881
+ }
1882
+
1883
+ // ์นด๋“œ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ ์ง์ ‘ ์˜คํ”ˆ
1884
+ if (!foundCard) {
1885
+ toggle(false);
1886
+ showLoading("PDF ์ค€๋น„ ์ค‘...");
1887
+
1888
+ let pages = [];
1889
+
1890
+ // ์ด๋ฏธ ์บ์‹œ๋œ ๊ฒฝ์šฐ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
1891
+ if (isCached) {
1892
+ try {
1893
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
1894
+ const cachedData = await response.json();
1895
+
1896
+ if (cachedData.status === "completed" && cachedData.pages) {
1897
+ hideLoading();
1898
+ createFlipBook(cachedData.pages);
1899
+ // ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID ์ €์žฅ
1900
+ currentPdfId = pdfId;
1901
+ return;
1902
+ }
1903
+ } catch (error) {
1904
+ console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error);
1905
+ }
1906
+ }
1907
+
1908
+ // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
1909
+ try {
1910
+ const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdfPath)}`);
1911
+ const thumbData = await thumbResponse.json();
1912
+
1913
+ if (thumbData.thumbnail) {
1914
+ pages = [{
1915
+ src: thumbData.thumbnail,
1916
+ thumb: thumbData.thumbnail,
1917
+ path: pdfPath,
1918
+ cached: isCached
1919
+ }];
1920
+ }
1921
+ } catch (error) {
1922
+ console.error("์ธ๋„ค์ผ ๋กœ๋“œ ์‹คํŒจ:", error);
1923
+ }
1924
+
1925
+ // ์ผ๋‹จ ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ์ถ”๊ฐ€
1926
+ if (pages.length === 0) {
1927
+ pages = [{
1928
+ path: pdfPath,
1929
+ cached: isCached
1930
+ }];
1931
+ }
1932
+
1933
+ // ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ณ  ๋ทฐ์–ด ์‹คํ–‰
1934
+ const projectId = projects.push(pages) - 1;
1935
+ hideLoading();
1936
+ open(projectId);
1937
+
1938
+ // ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID ์ €์žฅ
1939
+ currentPdfId = pdfId;
1940
+ }
1941
+ } catch (error) {
1942
+ console.error("PDF ID๋กœ ์—ด๊ธฐ ์‹คํŒจ:", error);
1943
+ hideLoading();
1944
+ showError("PDF๋ฅผ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
1945
+ }
1946
+ }
1947
+
1948
+ /* โ”€โ”€ ํ˜„์žฌ PDF์˜ ๊ณ ์œ  URL ์ƒ์„ฑ ๋ฐ ๋ณต์‚ฌ โ”€โ”€ */
1949
+ function copyPdfShareUrl() {
1950
+ if (!currentPdfId) {
1951
+ showError("๊ณต์œ ํ•  PDF๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
1952
+ return;
1953
+ }
1954
+
1955
+ // ํ˜„์žฌ ๋„๋ฉ”์ธ ๊ธฐ๋ฐ˜ ์ „์ฒด URL ์ƒ์„ฑ
1956
+ const shareUrl = `${window.location.origin}/view/${currentPdfId}`;
1957
+
1958
+ // ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ
1959
+ navigator.clipboard.writeText(shareUrl)
1960
+ .then(() => {
1961
+ showMessage("PDF ๋งํฌ๊ฐ€ ๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
1962
+ })
1963
+ .catch(err => {
1964
+ console.error("ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ ์‹คํŒจ:", err);
1965
+ showError("๋งํฌ ๋ณต์‚ฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
1966
+ });
1967
+ }
1968
+
1969
  /* โ”€โ”€ ์นด๋“œ โ†’ FlipBook โ”€โ”€ */
1970
  async function open(i) {
1971
  toggle(false);
1972
  const pages = projects[i];
1973
 
1974
+ // PDF ID ์ฐพ๊ธฐ ๋ฐ ์ €์žฅ
1975
+ const card = document.querySelectorAll('.card')[i];
1976
+ if (card && card.dataset.pdfId) {
1977
+ currentPdfId = card.dataset.pdfId;
1978
+ } else {
1979
+ currentPdfId = null;
1980
+ }
1981
+
1982
  // ๊ธฐ์กด FlipBook ์ •๋ฆฌ
1983
  if(fb) {
1984
  fb.destroy();
 
2242
  enableDownload: false,
2243
  enablePrint: false,
2244
  enableZoom: true,
2245
+ enableShare: true, // ๊ณต์œ  ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
2246
  enableSearch: true,
2247
  enableAutoPlay: true,
2248
  enableAnnotation: false,
 
2259
  pageTextureSize: 1024, // ํŽ˜์ด์ง€ ํ…์Šค์ฒ˜ ํฌ๊ธฐ
2260
  thumbnails: true, // ์„ฌ๋„ค์ผ ํ™œ์„ฑํ™”
2261
  autoHideControls: false, // ์ž๋™ ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
2262
+ controlsTimeout: 8000, // ์ปจํŠธ๋กค ํ‘œ์‹œ ์‹œ๊ฐ„ ์—ฐ์žฅ
2263
+ shareHandler: copyPdfShareUrl // ๊ณต์œ  ํ•ธ๋“ค๋Ÿฌ ์„ค์ •
2264
  }
2265
  });
2266
 
 
2315
  }
2316
  $id('loadingPages').style.display = 'none';
2317
  currentLoadingPdfPath = null;
2318
+ currentPdfId = null;
2319
  };
2320
 
2321
  function toggle(showHome){
 
2430
  const card = document.createElement('div');
2431
  card.className = 'admin-card card fade-in';
2432
 
2433
+ // ๊ณ ์œ  URL ์ƒ์„ฑ
2434
+ const viewUrl = `${window.location.origin}/view/${pdf.id}`;
2435
+
2436
  // ์ธ๋„ค์ผ ๋ฐ ์ •๋ณด
2437
  card.innerHTML = `
2438
  <div class="card-inner">
2439
  ${pdf.cached ? '<div class="cached-status">์บ์‹œ๋จ</div>' : ''}
2440
  <img src="${thumbData.thumbnail || ''}" alt="${pdf.name}" loading="lazy">
2441
  <p title="${pdf.name}">${pdf.name.length > 15 ? pdf.name.substring(0, 15) + '...' : pdf.name}</p>
2442
+ <div style="position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%); z-index:10;">
2443
+ <a href="${viewUrl}" target="_blank" style="color:#4a6ee0; font-size:12px;">๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ</a>
2444
+ </div>
2445
  ${isMainDisplayed ?
2446
  `<button class="unfeature-btn" data-path="${pdf.path}">๋ฉ”์ธ์—์„œ ์ œ๊ฑฐ</button>` :
2447
  `<button class="feature-btn" data-path="${pdf.path}">๋ฉ”์ธ์— ํ‘œ์‹œ</button>`}
 
2696
  </html>
2697
  """
2698
 
 
 
 
 
2699
  if __name__ == "__main__":
2700
  uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))