ginipick commited on
Commit
1b9f861
ยท
verified ยท
1 Parent(s): 085ae59

Update app-backup4.py

Browse files
Files changed (1) hide show
  1. app-backup4.py +748 -94
app-backup4.py CHANGED
@@ -1,7 +1,7 @@
1
- from fastapi import FastAPI, BackgroundTasks
2
  from fastapi.responses import HTMLResponse, JSONResponse, Response
3
  from fastapi.staticfiles import StaticFiles
4
- import pathlib, os, uvicorn, base64, json
5
  from typing import Dict, List, Any
6
  import asyncio
7
  import logging
@@ -21,23 +21,38 @@ PDF_DIR = BASE / "pdf"
21
  if not PDF_DIR.exists():
22
  PDF_DIR.mkdir(parents=True)
23
 
 
 
 
 
 
24
  # ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
25
  CACHE_DIR = BASE / "cache"
26
  if not CACHE_DIR.exists():
27
  CACHE_DIR.mkdir(parents=True)
28
 
 
 
 
29
  # ์ „์—ญ ์บ์‹œ ๊ฐ์ฒด
30
  pdf_cache: Dict[str, Dict[str, Any]] = {}
31
  # ์บ์‹ฑ ๋ฝ
32
  cache_locks = {}
33
 
34
- # PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
35
  def get_pdf_files():
36
  pdf_files = []
37
  if PDF_DIR.exists():
38
  pdf_files = [f for f in PDF_DIR.glob("*.pdf")]
39
  return pdf_files
40
 
 
 
 
 
 
 
 
41
  # PDF ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
42
  def generate_pdf_projects():
43
  projects_data = []
@@ -213,7 +228,13 @@ async def cache_pdf(pdf_path: str):
213
  # ์‹œ์ž‘ ์‹œ ๋ชจ๋“  PDF ํŒŒ์ผ ์บ์‹ฑ
214
  async def init_cache_all_pdfs():
215
  logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
216
- pdf_files = get_pdf_files()
 
 
 
 
 
 
217
 
218
  # ์ด๋ฏธ ์บ์‹œ๋œ PDF ํŒŒ์ผ ๋กœ๋“œ (๋น ๋ฅธ ์‹œ์ž‘์„ ์œ„ํ•ด ๋จผ์ € ์ˆ˜ํ–‰)
219
  for cache_file in CACHE_DIR.glob("*_cache.json"):
@@ -245,6 +266,21 @@ async def startup_event():
245
  async def get_pdf_projects_api():
246
  return generate_pdf_projects()
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  # API ์—”๋“œํฌ์ธํŠธ: PDF ์ธ๋„ค์ผ ์ œ๊ณต (์ตœ์ ํ™”)
249
  @app.get("/api/pdf-thumbnail")
250
  async def get_pdf_thumbnail(path: str):
@@ -369,6 +405,110 @@ async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
369
  logger.error(f"PDF ์ฝ˜ํ…์ธ  ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
370
  return JSONResponse(content={"error": str(e)}, status_code=500)
371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  # HTML ํŒŒ์ผ ์ฝ๊ธฐ ํ•จ์ˆ˜
373
  def get_html_content():
374
  html_path = BASE / "flipbook_template.html"
@@ -415,14 +555,21 @@ HTML = """
415
  --transition: all 0.3s ease;
416
  }
417
 
418
- body {
419
- margin: 0;
420
- background: var(--bg-color);
421
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
422
- color: var(--text-color);
423
- background-image: linear-gradient(120deg, var(--tertiary-color) 0%, var(--bg-color) 100%);
424
- background-attachment: fixed;
425
- }
 
 
 
 
 
 
 
426
 
427
  /* ํ—ค๋” ์ œ๋ชฉ ์ œ๊ฑฐ ๋ฐ Home ๋ฒ„ํŠผ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ */
428
  .floating-home {
@@ -450,6 +597,11 @@ HTML = """
450
  }
451
 
452
  .floating-home .icon {
 
 
 
 
 
453
  font-size: 22px;
454
  color: var(--primary-color);
455
  transition: var(--transition);
@@ -480,7 +632,99 @@ HTML = """
480
  transform: translateX(0);
481
  }
482
 
483
- #home, #viewerPage {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  padding-top: 100px;
485
  max-width: 1200px;
486
  margin: 0 auto;
@@ -649,6 +893,51 @@ HTML = """
649
  z-index: 5;
650
  box-shadow: var(--shadow-sm);
651
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
  /* ๋ทฐ์–ด ์Šคํƒ€์ผ */
654
  #viewer {
@@ -798,7 +1087,7 @@ HTML = """
798
  /* ํ—ค๋” ๋กœ๊ณ  ๋ฐ ํƒ€์ดํ‹€ */
799
  .library-header {
800
  position: fixed;
801
- top: 20px;
802
  left: 0;
803
  right: 0;
804
  text-align: center;
@@ -806,21 +1095,21 @@ HTML = """
806
  pointer-events: none;
807
  }
808
 
809
- .library-header .title {
810
- display: inline-block;
811
- padding: 12px 30px;
812
- background: rgba(255, 255, 255, 0.85);
813
- backdrop-filter: blur(10px);
814
- border-radius: 30px;
815
- box-shadow: var(--shadow-md);
816
- font-size: 1.5rem;
817
- font-weight: 600;
818
- background-image: linear-gradient(120deg, #667eea 0%, #764ba2 100%);
819
- -webkit-background-clip: text;
820
- background-clip: text;
821
- color: transparent;
822
- pointer-events: all;
823
- }
824
 
825
  /* ์ ์ง„์  ๋กœ๋”ฉ ํ‘œ์‹œ */
826
  .loading-pages {
@@ -837,6 +1126,50 @@ HTML = """
837
  z-index: 9998;
838
  text-align: center;
839
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840
 
841
  /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
842
  @media (max-width: 768px) {
@@ -862,6 +1195,11 @@ HTML = """
862
  .floating-home .icon {
863
  font-size: 18px;
864
  }
 
 
 
 
 
865
  }
866
  </style>
867
  </head>
@@ -872,21 +1210,34 @@ HTML = """
872
  <div class="title">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</div>
873
  </div>
874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
875
  <!-- ์„ผํ„ฐ ์ •๋ ฌ๋œ ํƒ€์ดํ‹€ -->
876
  <div class="library-header">
877
- <div class="title">FlipBook Library</div>
878
  </div>
879
 
880
  <section id="home" class="fade-in">
881
  <div class="upload-container">
882
- <button class="upload">
883
- <i class="fas fa-images"></i> ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
884
- <input id="imgInput" type="file" accept="image/*" multiple hidden>
885
- </button>
886
- <button class="upload">
887
  <i class="fas fa-file-pdf"></i> PDF ์ถ”๊ฐ€
888
- <input id="pdfInput" type="file" accept="application/pdf" hidden>
889
  </button>
 
890
  </div>
891
 
892
  <div class="section-title">๋‚ด ํ”„๋กœ์ ํŠธ</div>
@@ -894,7 +1245,7 @@ HTML = """
894
  <!-- ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
895
  </div>
896
  <div id="noProjects" class="no-projects" style="display: none;">
897
- ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๋‚˜ PDF๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
898
  </div>
899
  </section>
900
 
@@ -902,10 +1253,24 @@ HTML = """
902
  <div id="viewer"></div>
903
  <div id="loadingPages" class="loading-pages" style="display:none;">ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0/0</span></div>
904
  </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
 
906
  <script>
907
  let projects=[], fb=null;
908
- const grid=$id('grid'), viewer=$id('viewer');
909
  pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
910
 
911
  // ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
@@ -921,10 +1286,84 @@ HTML = """
921
  .play().then(a=>a.pause()).catch(()=>{});document.removeEventListener(evt,u,{capture:true});},
922
  {once:true,capture:true});
923
  });
924
-
925
  /* โ”€โ”€ ์œ ํ‹ธ โ”€โ”€ */
926
  function $id(id){return document.getElementById(id)}
927
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
928
  function addCard(i, thumb, title, isCached = false) {
929
  const d = document.createElement('div');
930
  d.className = 'card fade-in';
@@ -952,54 +1391,6 @@ HTML = """
952
  $id('noProjects').style.display = 'none';
953
  }
954
 
955
- /* โ”€โ”€ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ โ”€โ”€ */
956
- $id('imgInput').onchange=e=>{
957
- const files=[...e.target.files]; if(!files.length) return;
958
-
959
- // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
960
- showLoading("์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘...");
961
-
962
- const pages=[],tot=files.length;let done=0;
963
- files.forEach((f,i)=>{const r=new FileReader();r.onload=x=>{pages[i]={src:x.target.result,thumb:x.target.result};
964
- if(++done===tot) {
965
- save(pages, '์ด๋ฏธ์ง€ ์ปฌ๋ ‰์…˜');
966
- hideLoading();
967
- }
968
- };r.readAsDataURL(f);});
969
- };
970
-
971
- /* โ”€โ”€ PDF ์—…๋กœ๋“œ โ”€โ”€ */
972
- $id('pdfInput').onchange=e=>{
973
- const file=e.target.files[0]; if(!file) return;
974
-
975
- // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
976
- showLoading("PDF ๋กœ๋”ฉ ์ค‘...");
977
-
978
- const fr=new FileReader();
979
- fr.onload=v=>{
980
- pdfjsLib.getDocument({data:v.target.result}).promise.then(async pdf=>{
981
- const pages=[];
982
-
983
- for(let p=1;p<=pdf.numPages;p++){
984
- // ๋กœ๋”ฉ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
985
- updateLoading(`PDF ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... (${p}/${pdf.numPages})`);
986
-
987
- const pg=await pdf.getPage(p), vp=pg.getViewport({scale:1});
988
- const c=document.createElement('canvas');c.width=vp.width;c.height=vp.height;
989
- await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
990
- pages.push({src:c.toDataURL(),thumb:c.toDataURL()});
991
- }
992
-
993
- hideLoading();
994
- save(pages, file.name.replace('.pdf', ''));
995
- }).catch(error => {
996
- console.error("PDF ๋กœ๋”ฉ ์˜ค๋ฅ˜:", error);
997
- hideLoading();
998
- showError("PDF ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
999
- });
1000
- };fr.readAsArrayBuffer(file);
1001
- };
1002
-
1003
  /* โ”€โ”€ ํ”„๋กœ์ ํŠธ ์ €์žฅ โ”€โ”€ */
1004
  function save(pages, title, isCached = false){
1005
  const id=projects.push(pages)-1;
@@ -1022,6 +1413,10 @@ HTML = """
1022
  const response = await fetch('/api/pdf-projects');
1023
  serverProjects = await response.json();
1024
 
 
 
 
 
1025
  if (serverProjects.length === 0) {
1026
  hideLoading();
1027
  $id('noProjects').style.display = 'block';
@@ -1497,6 +1892,7 @@ HTML = """
1497
  $id('home').style.display=showHome?'block':'none';
1498
  $id('viewerPage').style.display=showHome?'none':'block';
1499
  $id('homeButton').style.display=showHome?'none':'block';
 
1500
 
1501
  // ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
1502
  if(!showHome) {
@@ -1505,6 +1901,242 @@ HTML = """
1505
  document.body.classList.remove('viewer-mode');
1506
  }
1507
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1508
 
1509
  /* -- ๋กœ๋”ฉ ๋ฐ ์˜ค๋ฅ˜ ํ‘œ์‹œ -- */
1510
  function showLoading(message, progress = -1) {
@@ -1593,14 +2225,36 @@ HTML = """
1593
  }
1594
  }, 5000);
1595
  }
1596
-
1597
- // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
1598
- window.addEventListener('DOMContentLoaded', () => {
1599
- loadServerPDFs();
 
 
 
1600
 
1601
- // ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ (3์ดˆ๋งˆ๋‹ค)
1602
- setInterval(checkCacheStatus, 3000);
1603
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1604
  </script>
1605
  </body>
1606
  </html>
 
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
 
21
  if not PDF_DIR.exists():
22
  PDF_DIR.mkdir(parents=True)
23
 
24
+ # ์˜๊ตฌ PDF ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • (Hugging Face ์˜๊ตฌ ๋””์Šคํฌ)
25
+ PERMANENT_PDF_DIR = pathlib.Path("/data/pdfs") if os.path.exists("/data") else BASE / "permanent_pdfs"
26
+ if not PERMANENT_PDF_DIR.exists():
27
+ PERMANENT_PDF_DIR.mkdir(parents=True)
28
+
29
  # ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
30
  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
+
37
  # ์ „์—ญ ์บ์‹œ ๊ฐ์ฒด
38
  pdf_cache: Dict[str, Dict[str, Any]] = {}
39
  # ์บ์‹ฑ ๋ฝ
40
  cache_locks = {}
41
 
42
+ # PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ (๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์šฉ)
43
  def get_pdf_files():
44
  pdf_files = []
45
  if PDF_DIR.exists():
46
  pdf_files = [f for f in PDF_DIR.glob("*.pdf")]
47
  return pdf_files
48
 
49
+ # ์˜๊ตฌ ์ €์žฅ์†Œ์˜ PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
50
+ def get_permanent_pdf_files():
51
+ pdf_files = []
52
+ if PERMANENT_PDF_DIR.exists():
53
+ pdf_files = [f for f in PERMANENT_PDF_DIR.glob("*.pdf")]
54
+ return pdf_files
55
+
56
  # PDF ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
57
  def generate_pdf_projects():
58
  projects_data = []
 
228
  # ์‹œ์ž‘ ์‹œ ๋ชจ๋“  PDF ํŒŒ์ผ ์บ์‹ฑ
229
  async def init_cache_all_pdfs():
230
  logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
231
+
232
+ # ๋ฉ”์ธ ๋ฐ ์˜๊ตฌ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ PDF ํŒŒ์ผ ๋ชจ๋‘ ๊ฐ€์ ธ์˜ค๊ธฐ
233
+ pdf_files = get_pdf_files() + get_permanent_pdf_files()
234
+
235
+ # ์ค‘๋ณต ์ œ๊ฑฐ
236
+ unique_pdf_paths = set(str(p) for p in pdf_files)
237
+ pdf_files = [pathlib.Path(p) for p in unique_pdf_paths]
238
 
239
  # ์ด๋ฏธ ์บ์‹œ๋œ PDF ํŒŒ์ผ ๋กœ๋“œ (๋น ๋ฅธ ์‹œ์ž‘์„ ์œ„ํ•ด ๋จผ์ € ์ˆ˜ํ–‰)
240
  for cache_file in CACHE_DIR.glob("*_cache.json"):
 
266
  async def get_pdf_projects_api():
267
  return generate_pdf_projects()
268
 
269
+ # API ์—”๋“œํฌ์ธํŠธ: ์˜๊ตฌ ์ €์žฅ๋œ PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก
270
+ @app.get("/api/permanent-pdf-projects")
271
+ async def get_permanent_pdf_projects_api():
272
+ pdf_files = get_permanent_pdf_files()
273
+ projects_data = []
274
+
275
+ for pdf_file in pdf_files:
276
+ projects_data.append({
277
+ "path": str(pdf_file),
278
+ "name": pdf_file.stem,
279
+ "cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
280
+ })
281
+
282
+ return projects_data
283
+
284
  # API ์—”๋“œํฌ์ธํŠธ: PDF ์ธ๋„ค์ผ ์ œ๊ณต (์ตœ์ ํ™”)
285
  @app.get("/api/pdf-thumbnail")
286
  async def get_pdf_thumbnail(path: str):
 
405
  logger.error(f"PDF ์ฝ˜ํ…์ธ  ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
406
  return JSONResponse(content={"error": str(e)}, status_code=500)
407
 
408
+ # PDF ์—…๋กœ๋“œ ์—”๋“œํฌ์ธํŠธ - ์˜๊ตฌ ์ €์žฅ์†Œ์— ์ €์žฅ
409
+ @app.post("/api/upload-pdf")
410
+ async def upload_pdf(file: UploadFile = File(...)):
411
+ try:
412
+ # ํŒŒ์ผ ์ด๋ฆ„ ํ™•์ธ
413
+ if not file.filename.lower().endswith('.pdf'):
414
+ return JSONResponse(
415
+ content={"success": False, "message": "PDF ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค"},
416
+ status_code=400
417
+ )
418
+
419
+ # ์˜๊ตฌ ์ €์žฅ์†Œ์— ํŒŒ์ผ ์ €์žฅ
420
+ file_path = PERMANENT_PDF_DIR / file.filename
421
+
422
+ # ํŒŒ์ผ ์ฝ๊ธฐ ๋ฐ ์ €์žฅ
423
+ content = await file.read()
424
+ with open(file_path, "wb") as buffer:
425
+ buffer.write(content)
426
+
427
+ # ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์—๋„ ๋ณต์‚ฌ (ํ•„์š”์‹œ ์„ ํƒ ๊ฐ€๋Šฅ, ๊ด€๋ฆฌ์ž๊ฐ€ ์„ ํƒํ•œ PDF๋งŒ ํ‘œ์‹œ)
428
+ # with open(PDF_DIR / file.filename, "wb") as buffer:
429
+ # buffer.write(content)
430
+
431
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
432
+ asyncio.create_task(cache_pdf(str(file_path)))
433
+
434
+ return JSONResponse(
435
+ content={"success": True, "path": str(file_path), "name": file_path.stem},
436
+ status_code=200
437
+ )
438
+ except Exception as e:
439
+ import traceback
440
+ error_details = traceback.format_exc()
441
+ logger.error(f"PDF ์—…๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
442
+ return JSONResponse(
443
+ content={"success": False, "message": str(e)},
444
+ status_code=500
445
+ )
446
+
447
+ # ๊ด€๋ฆฌ์ž ์ธ์ฆ ์—”๋“œํฌ์ธํŠธ
448
+ @app.post("/api/admin-login")
449
+ async def admin_login(password: str = Form(...)):
450
+ if password == ADMIN_PASSWORD:
451
+ return {"success": True}
452
+ return {"success": False, "message": "์ธ์ฆ ์‹คํŒจ"}
453
+
454
+ # ๊ด€๋ฆฌ์ž์šฉ PDF ์‚ญ์ œ ์—”๋“œํฌ์ธํŠธ
455
+ @app.delete("/api/admin/delete-pdf")
456
+ async def delete_pdf(path: str):
457
+ try:
458
+ pdf_file = pathlib.Path(path)
459
+ if not pdf_file.exists():
460
+ return {"success": False, "message": "ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}
461
+
462
+ # PDF ํŒŒ์ผ ์‚ญ์ œ
463
+ pdf_file.unlink()
464
+
465
+ # ๊ด€๋ จ ์บ์‹œ ํŒŒ์ผ ์‚ญ์ œ
466
+ pdf_name = pdf_file.stem
467
+ cache_path = get_cache_path(pdf_name)
468
+ if cache_path.exists():
469
+ cache_path.unlink()
470
+
471
+ # ์บ์‹œ ๋ฉ”๋ชจ๋ฆฌ์—์„œ๋„ ์ œ๊ฑฐ
472
+ if pdf_name in pdf_cache:
473
+ del pdf_cache[pdf_name]
474
+
475
+ return {"success": True}
476
+ except Exception as e:
477
+ logger.error(f"PDF ์‚ญ์ œ ์˜ค๋ฅ˜: {str(e)}")
478
+ return {"success": False, "message": str(e)}
479
+
480
+ # PDF๋ฅผ ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์— ํ‘œ์‹œ ์„ค์ •
481
+ @app.post("/api/admin/feature-pdf")
482
+ async def feature_pdf(path: str):
483
+ try:
484
+ pdf_file = pathlib.Path(path)
485
+ if not pdf_file.exists():
486
+ return {"success": False, "message": "ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}
487
+
488
+ # ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์— ๋ณต์‚ฌ
489
+ target_path = PDF_DIR / pdf_file.name
490
+ shutil.copy2(pdf_file, target_path)
491
+
492
+ return {"success": True}
493
+ except Exception as e:
494
+ logger.error(f"PDF ํ‘œ์‹œ ์„ค์ • ์˜ค๋ฅ˜: {str(e)}")
495
+ return {"success": False, "message": str(e)}
496
+
497
+ # PDF๋ฅผ ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ์ œ๊ฑฐ (์˜๊ตฌ ์ €์žฅ์†Œ์—์„œ๋Š” ์œ ์ง€)
498
+ @app.delete("/api/admin/unfeature-pdf")
499
+ async def unfeature_pdf(path: str):
500
+ try:
501
+ pdf_name = pathlib.Path(path).name
502
+ target_path = PDF_DIR / pdf_name
503
+
504
+ if target_path.exists():
505
+ target_path.unlink()
506
+
507
+ return {"success": True}
508
+ except Exception as e:
509
+ logger.error(f"PDF ํ‘œ์‹œ ํ•ด์ œ ์˜ค๋ฅ˜: {str(e)}")
510
+ return {"success": False, "message": str(e)}
511
+
512
  # HTML ํŒŒ์ผ ์ฝ๊ธฐ ํ•จ์ˆ˜
513
  def get_html_content():
514
  html_path = BASE / "flipbook_template.html"
 
555
  --transition: all 0.3s ease;
556
  }
557
 
558
+
559
+ body {
560
+ margin: 0;
561
+ background: var(--bg-color);
562
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
563
+ color: var(--text-color);
564
+ /* ์ƒˆ๋กœ์šด ํผํ”Œ ๊ณ„ํ†ต ๊ณ ๊ธ‰์Šค๋Ÿฌ์šด ๊ทธ๋ผ๋””์—์ด์…˜ ๋ฐฐ๊ฒฝ */
565
+ background-image: linear-gradient(135deg, #2a0845 0%, #6441a5 50%, #c9a8ff 100%);
566
+ background-attachment: fixed;
567
+ }
568
+
569
+ /* ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ๋ฐฐ๊ฒฝ ๋ณ€๊ฒฝ */
570
+ .viewer-mode {
571
+ background-image: linear-gradient(135deg, #30154e 0%, #6b47ad 50%, #d5b8ff 100%) !important;
572
+ }
573
 
574
  /* ํ—ค๋” ์ œ๋ชฉ ์ œ๊ฑฐ ๋ฐ Home ๋ฒ„ํŠผ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ */
575
  .floating-home {
 
597
  }
598
 
599
  .floating-home .icon {
600
+ display: flex;
601
+ justify-content: center;
602
+ align-items: center;
603
+ width: 100%;
604
+ height: 100%;
605
  font-size: 22px;
606
  color: var(--primary-color);
607
  transition: var(--transition);
 
632
  transform: translateX(0);
633
  }
634
 
635
+ /* ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
636
+ #adminButton {
637
+ position: fixed;
638
+ top: 20px;
639
+ right: 20px;
640
+ background: rgba(255, 255, 255, 0.9);
641
+ backdrop-filter: blur(10px);
642
+ box-shadow: var(--shadow-md);
643
+ border-radius: 30px;
644
+ padding: 8px 20px;
645
+ display: flex;
646
+ align-items: center;
647
+ font-weight: 600;
648
+ font-size: 14px;
649
+ cursor: pointer;
650
+ transition: var(--transition);
651
+ z-index: 9999;
652
+ }
653
+
654
+ #adminButton i {
655
+ margin-right: 8px;
656
+ color: var(--accent-color);
657
+ }
658
+
659
+ #adminButton:hover {
660
+ transform: translateY(-3px);
661
+ box-shadow: var(--shadow-lg);
662
+ }
663
+
664
+ /* ๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ */
665
+ .modal {
666
+ display: none;
667
+ position: fixed;
668
+ top: 0;
669
+ left: 0;
670
+ width: 100%;
671
+ height: 100%;
672
+ background: rgba(0, 0, 0, 0.5);
673
+ backdrop-filter: blur(5px);
674
+ z-index: 10000;
675
+ align-items: center;
676
+ justify-content: center;
677
+ }
678
+
679
+ .modal-content {
680
+ background: white;
681
+ border-radius: var(--radius-md);
682
+ padding: 30px;
683
+ width: 90%;
684
+ max-width: 400px;
685
+ box-shadow: var(--shadow-lg);
686
+ text-align: center;
687
+ }
688
+
689
+ .modal-content h2 {
690
+ margin-top: 0;
691
+ color: var(--accent-color);
692
+ margin-bottom: 20px;
693
+ }
694
+
695
+ .modal-content input {
696
+ width: 100%;
697
+ padding: 12px;
698
+ margin-bottom: 20px;
699
+ border: 1px solid #ddd;
700
+ border-radius: var(--radius-sm);
701
+ font-size: 16px;
702
+ box-sizing: border-box;
703
+ }
704
+
705
+ .modal-content button {
706
+ padding: 10px 20px;
707
+ border: none;
708
+ border-radius: var(--radius-sm);
709
+ background: var(--accent-color);
710
+ color: white;
711
+ font-weight: 600;
712
+ cursor: pointer;
713
+ margin: 0 5px;
714
+ transition: var(--transition);
715
+ }
716
+
717
+ .modal-content button:hover {
718
+ opacity: 0.9;
719
+ transform: translateY(-2px);
720
+ }
721
+
722
+ .modal-content #adminLoginClose {
723
+ background: #f1f3f5;
724
+ color: var(--text-color);
725
+ }
726
+
727
+ #home, #viewerPage, #adminPage {
728
  padding-top: 100px;
729
  max-width: 1200px;
730
  margin: 0 auto;
 
893
  z-index: 5;
894
  box-shadow: var(--shadow-sm);
895
  }
896
+
897
+ /* ๊ด€๋ฆฌ์ž ์นด๋“œ ์ถ”๊ฐ€ ์Šคํƒ€์ผ */
898
+ .admin-card {
899
+ height: 300px;
900
+ }
901
+
902
+ .admin-card .card-inner {
903
+ width: 100%;
904
+ height: 100%;
905
+ position: relative;
906
+ display: flex;
907
+ flex-direction: column;
908
+ align-items: center;
909
+ }
910
+
911
+ .delete-btn, .feature-btn, .unfeature-btn {
912
+ position: absolute;
913
+ bottom: 60px;
914
+ left: 50%;
915
+ transform: translateX(-50%);
916
+ background: #ff7675;
917
+ color: white;
918
+ border: none;
919
+ border-radius: 20px;
920
+ padding: 5px 15px;
921
+ font-size: 12px;
922
+ cursor: pointer;
923
+ z-index: 10;
924
+ transition: var(--transition);
925
+ }
926
+
927
+ .feature-btn {
928
+ bottom: 95px;
929
+ background: #74b9ff;
930
+ }
931
+
932
+ .unfeature-btn {
933
+ bottom: 95px;
934
+ background: #a29bfe;
935
+ }
936
+
937
+ .delete-btn:hover, .feature-btn:hover, .unfeature-btn:hover {
938
+ opacity: 0.9;
939
+ transform: translateX(-50%) scale(1.05);
940
+ }
941
 
942
  /* ๋ทฐ์–ด ์Šคํƒ€์ผ */
943
  #viewer {
 
1087
  /* ํ—ค๋” ๋กœ๊ณ  ๋ฐ ํƒ€์ดํ‹€ */
1088
  .library-header {
1089
  position: fixed;
1090
+ top: 12px;
1091
  left: 0;
1092
  right: 0;
1093
  text-align: center;
 
1095
  pointer-events: none;
1096
  }
1097
 
1098
+ .library-header .title {
1099
+ display: inline-block;
1100
+ padding: 8px 24px; /* ํŒจ๋”ฉ ์ถ•์†Œ */
1101
+ background: rgba(255, 255, 255, 0.85);
1102
+ backdrop-filter: blur(10px);
1103
+ border-radius: 25px; /* ํ…Œ๋‘๋ฆฌ ๋ชจ์„œ๋ฆฌ ์ถ•์†Œ */
1104
+ box-shadow: var(--shadow-md);
1105
+ font-size: 1.25rem; /* ๊ธ€์ž ํฌ๊ธฐ ์ถ•์†Œ (1.5rem์—์„œ 1.25rem์œผ๋กœ) */
1106
+ font-weight: 600;
1107
+ background-image: linear-gradient(120deg, #8e74eb 0%, #9d66ff 100%); /* ์ œ๋ชฉ ์ƒ‰์ƒ๋„ ๋ฐ”ํƒ•ํ™”๋ฉด๊ณผ ์–ด์šธ๋ฆฌ๊ฒŒ ๋ณ€๊ฒฝ */
1108
+ -webkit-background-clip: text;
1109
+ background-clip: text;
1110
+ color: transparent;
1111
+ pointer-events: all;
1112
+ }
1113
 
1114
  /* ์ ์ง„์  ๋กœ๋”ฉ ํ‘œ์‹œ */
1115
  .loading-pages {
 
1126
  z-index: 9998;
1127
  text-align: center;
1128
  }
1129
+
1130
+ /* ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์Šคํƒ€์ผ */
1131
+ #adminPage {
1132
+ color: white;
1133
+ max-width: 1400px;
1134
+ }
1135
+
1136
+ #adminPage h1 {
1137
+ font-size: 2rem;
1138
+ margin-bottom: 30px;
1139
+ text-align: center;
1140
+ background-image: linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%);
1141
+ -webkit-background-clip: text;
1142
+ background-clip: text;
1143
+ color: transparent;
1144
+ }
1145
+
1146
+ #adminBackButton {
1147
+ position: absolute;
1148
+ top: 20px;
1149
+ left: 20px;
1150
+ background: rgba(255, 255, 255, 0.9);
1151
+ backdrop-filter: blur(10px);
1152
+ box-shadow: var(--shadow-md);
1153
+ border: none;
1154
+ border-radius: 30px;
1155
+ padding: 8px 20px;
1156
+ display: flex;
1157
+ align-items: center;
1158
+ font-weight: 600;
1159
+ font-size: 14px;
1160
+ cursor: pointer;
1161
+ transition: var(--transition);
1162
+ }
1163
+
1164
+ #adminBackButton:hover {
1165
+ transform: translateY(-3px);
1166
+ box-shadow: var(--shadow-lg);
1167
+ }
1168
+
1169
+ /* ๊ด€๋ฆฌ์ž ๊ทธ๋ฆฌ๋“œ */
1170
+ #adminGrid {
1171
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
1172
+ }
1173
 
1174
  /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
1175
  @media (max-width: 768px) {
 
1195
  .floating-home .icon {
1196
  font-size: 18px;
1197
  }
1198
+
1199
+ #adminButton {
1200
+ padding: 6px 15px;
1201
+ font-size: 12px;
1202
+ }
1203
  }
1204
  </style>
1205
  </head>
 
1210
  <div class="title">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</div>
1211
  </div>
1212
 
1213
+ <!-- ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ -->
1214
+ <div id="adminButton">
1215
+ <i class="fas fa-cog"></i> Admin
1216
+ </div>
1217
+
1218
+ <!-- ๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ -->
1219
+ <div id="adminLoginModal" class="modal">
1220
+ <div class="modal-content">
1221
+ <h2>๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ</h2>
1222
+ <input type="password" id="adminPasswordInput" placeholder="๊ด€๋ฆฌ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ">
1223
+ <div>
1224
+ <button id="adminLoginButton">๋กœ๊ทธ์ธ</button>
1225
+ <button id="adminLoginClose">์ทจ์†Œ</button>
1226
+ </div>
1227
+ </div>
1228
+ </div>
1229
+
1230
  <!-- ์„ผํ„ฐ ์ •๋ ฌ๋œ ํƒ€์ดํ‹€ -->
1231
  <div class="library-header">
1232
+ <div class="title">AI FlipBook Maker</div>
1233
  </div>
1234
 
1235
  <section id="home" class="fade-in">
1236
  <div class="upload-container">
1237
+ <button class="upload" id="pdfUploadBtn">
 
 
 
 
1238
  <i class="fas fa-file-pdf"></i> PDF ์ถ”๊ฐ€
 
1239
  </button>
1240
+ <input id="pdfInput" type="file" accept="application/pdf" style="display:none">
1241
  </div>
1242
 
1243
  <div class="section-title">๋‚ด ํ”„๋กœ์ ํŠธ</div>
 
1245
  <!-- ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
1246
  </div>
1247
  <div id="noProjects" class="no-projects" style="display: none;">
1248
+ ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. PDF๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
1249
  </div>
1250
  </section>
1251
 
 
1253
  <div id="viewer"></div>
1254
  <div id="loadingPages" class="loading-pages" style="display:none;">ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0/0</span></div>
1255
  </section>
1256
+
1257
+ <!-- ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ -->
1258
+ <section id="adminPage" style="display:none" class="fade-in">
1259
+ <h1>๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€</h1>
1260
+ <button id="adminBackButton"><i class="fas fa-arrow-left"></i> ๋’ค๋กœ ๊ฐ€๊ธฐ</button>
1261
+
1262
+ <div class="section-title">์ €์žฅ๋œ PDF ๋ชฉ๋ก</div>
1263
+ <div class="grid" id="adminGrid">
1264
+ <!-- ๊ด€๋ฆฌ์ž PDF ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
1265
+ </div>
1266
+ <div id="noAdminProjects" class="no-projects" style="display: none;">
1267
+ ์ €์žฅ๋œ PDF๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. PDF๋ฅผ ์—…๋กœ๋“œํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
1268
+ </div>
1269
+ </section>
1270
 
1271
  <script>
1272
  let projects=[], fb=null;
1273
+ const grid=document.getElementById('grid'), viewer=document.getElementById('viewer');
1274
  pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
1275
 
1276
  // ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
 
1286
  .play().then(a=>a.pause()).catch(()=>{});document.removeEventListener(evt,u,{capture:true});},
1287
  {once:true,capture:true});
1288
  });
1289
+
1290
  /* โ”€โ”€ ์œ ํ‹ธ โ”€โ”€ */
1291
  function $id(id){return document.getElementById(id)}
1292
 
1293
+ // ํŒŒ์ผ ์—…๋กœ๋“œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
1294
+ document.addEventListener("DOMContentLoaded", function() {
1295
+ console.log("DOM ๋กœ๋“œ ์™„๋ฃŒ, ์ด๋ฒคํŠธ ์„ค์ • ์‹œ์ž‘");
1296
+
1297
+ // PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ
1298
+ const pdfBtn = document.getElementById('pdfUploadBtn');
1299
+ const pdfInput = document.getElementById('pdfInput');
1300
+
1301
+ if (pdfBtn && pdfInput) {
1302
+ console.log("PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ฐพ์Œ");
1303
+
1304
+ // ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํŒŒ์ผ ์ž…๋ ฅ ํŠธ๋ฆฌ๊ฑฐ
1305
+ pdfBtn.addEventListener('click', function() {
1306
+ console.log("PDF ๋ฒ„ํŠผ ํด๋ฆญ๋จ");
1307
+ pdfInput.click();
1308
+ });
1309
+
1310
+ // ํŒŒ์ผ ์„ ํƒ ์‹œ ์ฒ˜๋ฆฌ
1311
+ pdfInput.addEventListener('change', function(e) {
1312
+ console.log("PDF ํŒŒ์ผ ์„ ํƒ๋จ:", e.target.files.length);
1313
+ const file = e.target.files[0];
1314
+ if (!file) return;
1315
+
1316
+ // ์„œ๋ฒ„์— PDF ์—…๋กœ๋“œ (์˜๊ตฌ ์ €์žฅ์†Œ์— ์ €์žฅ)
1317
+ uploadPdfToServer(file);
1318
+ });
1319
+ } else {
1320
+ console.error("PDF ์—…๋กœ๋“œ ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ");
1321
+ }
1322
+
1323
+ // ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ
1324
+ loadServerPDFs();
1325
+
1326
+ // ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
1327
+ setInterval(checkCacheStatus, 3000);
1328
+
1329
+ // ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •
1330
+ setupAdminFunctions();
1331
+ });
1332
+
1333
+ // ์„œ๋ฒ„์— PDF ์—…๋กœ๋“œ ํ•จ์ˆ˜
1334
+ async function uploadPdfToServer(file) {
1335
+ try {
1336
+ showLoading("PDF ์—…๋กœ๋“œ ์ค‘...");
1337
+
1338
+ const formData = new FormData();
1339
+ formData.append('file', file);
1340
+
1341
+ const response = await fetch('/api/upload-pdf', {
1342
+ method: 'POST',
1343
+ body: formData
1344
+ });
1345
+
1346
+ const result = await response.json();
1347
+
1348
+ if (result.success) {
1349
+ hideLoading();
1350
+
1351
+ // ์—…๋กœ๋“œ ์„ฑ๊ณต ์‹œ ์„œ๋ฒ„ PDF ๋ฆฌ์ŠคํŠธ ๋ฆฌ๋กœ๋“œ
1352
+ await loadServerPDFs();
1353
+
1354
+ // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
1355
+ showMessage("PDF๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
1356
+ } else {
1357
+ hideLoading();
1358
+ showError("์—…๋กœ๋“œ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
1359
+ }
1360
+ } catch (error) {
1361
+ console.error("PDF ์—…๋กœ๋“œ ์˜ค๋ฅ˜:", error);
1362
+ hideLoading();
1363
+ showError("PDF ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1364
+ }
1365
+ }
1366
+
1367
  function addCard(i, thumb, title, isCached = false) {
1368
  const d = document.createElement('div');
1369
  d.className = 'card fade-in';
 
1391
  $id('noProjects').style.display = 'none';
1392
  }
1393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
  /* โ”€โ”€ ํ”„๋กœ์ ํŠธ ์ €์žฅ โ”€โ”€ */
1395
  function save(pages, title, isCached = false){
1396
  const id=projects.push(pages)-1;
 
1413
  const response = await fetch('/api/pdf-projects');
1414
  serverProjects = await response.json();
1415
 
1416
+ // ๊ธฐ์กด ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™”
1417
+ grid.innerHTML = '';
1418
+ projects = [];
1419
+
1420
  if (serverProjects.length === 0) {
1421
  hideLoading();
1422
  $id('noProjects').style.display = 'block';
 
1892
  $id('home').style.display=showHome?'block':'none';
1893
  $id('viewerPage').style.display=showHome?'none':'block';
1894
  $id('homeButton').style.display=showHome?'none':'block';
1895
+ $id('adminPage').style.display='none';
1896
 
1897
  // ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
1898
  if(!showHome) {
 
1901
  document.body.classList.remove('viewer-mode');
1902
  }
1903
  }
1904
+
1905
+ /* -- ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ -- */
1906
+ function setupAdminFunctions() {
1907
+ // ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ํด๋ฆญ - ๋ชจ๋‹ฌ ํ‘œ์‹œ
1908
+ $id('adminButton').addEventListener('click', function() {
1909
+ $id('adminLoginModal').style.display = 'flex';
1910
+ $id('adminPasswordInput').value = '';
1911
+ $id('adminPasswordInput').focus();
1912
+ });
1913
+
1914
+ // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ๋ฒ„ํŠผ
1915
+ $id('adminLoginClose').addEventListener('click', function() {
1916
+ $id('adminLoginModal').style.display = 'none';
1917
+ });
1918
+
1919
+ // ์—”ํ„ฐ ํ‚ค๋กœ ๋กœ๊ทธ์ธ
1920
+ $id('adminPasswordInput').addEventListener('keyup', function(e) {
1921
+ if (e.key === 'Enter') {
1922
+ $id('adminLoginButton').click();
1923
+ }
1924
+ });
1925
+
1926
+ // ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ
1927
+ $id('adminLoginButton').addEventListener('click', async function() {
1928
+ const password = $id('adminPasswordInput').value;
1929
+
1930
+ try {
1931
+ showLoading("๋กœ๊ทธ์ธ ์ค‘...");
1932
+
1933
+ const formData = new FormData();
1934
+ formData.append('password', password);
1935
+
1936
+ const response = await fetch('/api/admin-login', {
1937
+ method: 'POST',
1938
+ body: formData
1939
+ });
1940
+
1941
+ const data = await response.json();
1942
+
1943
+ hideLoading();
1944
+
1945
+ if (data.success) {
1946
+ // ๋กœ๊ทธ์ธ ์„ฑ๊ณต - ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
1947
+ $id('adminLoginModal').style.display = 'none';
1948
+ showAdminPage();
1949
+ } else {
1950
+ // ๋กœ๊ทธ์ธ ์‹คํŒจ
1951
+ showError("๊ด€๋ฆฌ์ž ์ธ์ฆ ์‹คํŒจ: ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
1952
+ }
1953
+ } catch (error) {
1954
+ console.error("๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:", error);
1955
+ hideLoading();
1956
+ showError("๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1957
+ }
1958
+ });
1959
+
1960
+ // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋’ค๋กœ๊ฐ€๊ธฐ
1961
+ $id('adminBackButton').addEventListener('click', function() {
1962
+ $id('adminPage').style.display = 'none';
1963
+ $id('home').style.display = 'block';
1964
+ });
1965
+ }
1966
+
1967
+ // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
1968
+ async function showAdminPage() {
1969
+ showLoading("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘...");
1970
+
1971
+ // ๋‹ค๋ฅธ ํŽ˜์ด์ง€ ์ˆจ๊ธฐ๊ธฐ
1972
+ $id('home').style.display = 'none';
1973
+ $id('viewerPage').style.display = 'none';
1974
+
1975
+ // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์˜ PDF ๋ชฉ๋ก ๋กœ๋“œ
1976
+ try {
1977
+ const response = await fetch('/api/permanent-pdf-projects');
1978
+ const data = await response.json();
1979
+
1980
+ const adminGrid = $id('adminGrid');
1981
+ adminGrid.innerHTML = ''; // ๊ธฐ์กด ๋‚ด์šฉ ์ง€์šฐ๊ธฐ
1982
+
1983
+ if (data.length === 0) {
1984
+ $id('noAdminProjects').style.display = 'block';
1985
+ } else {
1986
+ $id('noAdminProjects').style.display = 'none';
1987
+
1988
+ // ๊ฐ PDF ํŒŒ์ผ์— ๋Œ€ํ•œ ์นด๋“œ ์ƒ์„ฑ
1989
+ const thumbnailPromises = data.map(async (pdf) => {
1990
+ try {
1991
+ // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
1992
+ const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdf.path)}`);
1993
+ const thumbData = await thumbResponse.json();
1994
+
1995
+ // ํ‘œ์‹œ ์—ฌ๋ถ€ ํ™•์ธ (๋ฉ”์ธ ํŽ˜์ด์ง€์— ํ‘œ์‹œ๋˜๋Š”์ง€)
1996
+ const mainPdfPath = pdf.path.split('/').pop();
1997
+ const isMainDisplayed = serverProjects.some(p => p.path.includes(mainPdfPath));
1998
+
1999
+ // ๊ด€๋ฆฌ์ž ์นด๋“œ ์ƒ์„ฑ
2000
+ const card = document.createElement('div');
2001
+ card.className = 'admin-card card fade-in';
2002
+
2003
+ // ์ธ๋„ค์ผ ๋ฐ ์ •๋ณด
2004
+ card.innerHTML = `
2005
+ <div class="card-inner">
2006
+ ${pdf.cached ? '<div class="cached-status">์บ์‹œ๋จ</div>' : ''}
2007
+ <img src="${thumbData.thumbnail || ''}" alt="${pdf.name}" loading="lazy">
2008
+ <p title="${pdf.name}">${pdf.name.length > 15 ? pdf.name.substring(0, 15) + '...' : pdf.name}</p>
2009
+ ${isMainDisplayed ?
2010
+ `<button class="unfeature-btn" data-path="${pdf.path}">๋ฉ”์ธ์—์„œ ์ œ๊ฑฐ</button>` :
2011
+ `<button class="feature-btn" data-path="${pdf.path}">๋ฉ”์ธ์— ํ‘œ์‹œ</button>`}
2012
+ <button class="delete-btn" data-path="${pdf.path}">์‚ญ์ œ</button>
2013
+ </div>
2014
+ `;
2015
+
2016
+ adminGrid.appendChild(card);
2017
+
2018
+ // ์‚ญ์ œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
2019
+ const deleteBtn = card.querySelector('.delete-btn');
2020
+ if (deleteBtn) {
2021
+ deleteBtn.addEventListener('click', async function(e) {
2022
+ e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
2023
+
2024
+ if (confirm(`์ •๋ง "${pdf.name}" PDF๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`)) {
2025
+ try {
2026
+ showLoading("PDF ์‚ญ์ œ ์ค‘...");
2027
+
2028
+ const response = await fetch(`/api/admin/delete-pdf?path=${encodeURIComponent(pdf.path)}`, {
2029
+ method: 'DELETE'
2030
+ });
2031
+
2032
+ const result = await response.json();
2033
+
2034
+ hideLoading();
2035
+
2036
+ if (result.success) {
2037
+ card.remove();
2038
+ showMessage("PDF๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
2039
+
2040
+ // ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
2041
+ loadServerPDFs();
2042
+ } else {
2043
+ showError("์‚ญ์ œ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
2044
+ }
2045
+ } catch (error) {
2046
+ console.error("PDF ์‚ญ์ œ ์˜ค๋ฅ˜:", error);
2047
+ hideLoading();
2048
+ showError("PDF ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
2049
+ }
2050
+ }
2051
+ });
2052
+ }
2053
+
2054
+ // ๋ฉ”์ธ์— ํ‘œ์‹œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
2055
+ const featureBtn = card.querySelector('.feature-btn');
2056
+ if (featureBtn) {
2057
+ featureBtn.addEventListener('click', async function(e) {
2058
+ e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
2059
+
2060
+ try {
2061
+ showLoading("์ฒ˜๋ฆฌ ์ค‘...");
2062
+
2063
+ const response = await fetch(`/api/admin/feature-pdf?path=${encodeURIComponent(pdf.path)}`, {
2064
+ method: 'POST'
2065
+ });
2066
+
2067
+ const result = await response.json();
2068
+
2069
+ hideLoading();
2070
+
2071
+ if (result.success) {
2072
+ showMessage("PDF๊ฐ€ ๋ฉ”์ธ ํŽ˜์ด์ง€์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.");
2073
+ // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ
2074
+ showAdminPage();
2075
+ // ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
2076
+ loadServerPDFs();
2077
+ } else {
2078
+ showError("์ฒ˜๋ฆฌ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
2079
+ }
2080
+ } catch (error) {
2081
+ console.error("PDF ํ‘œ์‹œ ์„ค์ • ๏ฟฝ๏ฟฝ๋ฅ˜:", error);
2082
+ hideLoading();
2083
+ showError("์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
2084
+ }
2085
+ });
2086
+ }
2087
+
2088
+ // ๋ฉ”์ธ์—์„œ ์ œ๊ฑฐ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
2089
+ const unfeatureBtn = card.querySelector('.unfeature-btn');
2090
+ if (unfeatureBtn) {
2091
+ unfeatureBtn.addEventListener('click', async function(e) {
2092
+ e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
2093
+
2094
+ try {
2095
+ showLoading("์ฒ˜๋ฆฌ ์ค‘...");
2096
+
2097
+ const response = await fetch(`/api/admin/unfeature-pdf?path=${encodeURIComponent(pdf.path)}`, {
2098
+ method: 'DELETE'
2099
+ });
2100
+
2101
+ const result = await response.json();
2102
+
2103
+ hideLoading();
2104
+
2105
+ if (result.success) {
2106
+ showMessage("PDF๊ฐ€ ๋ฉ”์ธ ํŽ˜์ด์ง€์—์„œ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
2107
+ // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ
2108
+ showAdminPage();
2109
+ // ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
2110
+ loadServerPDFs();
2111
+ } else {
2112
+ showError("์ฒ˜๋ฆฌ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
2113
+ }
2114
+ } catch (error) {
2115
+ console.error("PDF ํ‘œ์‹œ ํ•ด์ œ ์˜ค๋ฅ˜:", error);
2116
+ hideLoading();
2117
+ showError("์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
2118
+ }
2119
+ });
2120
+ }
2121
+ } catch (error) {
2122
+ console.error(`PDF ${pdf.name} ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:`, error);
2123
+ }
2124
+ });
2125
+
2126
+ await Promise.all(thumbnailPromises);
2127
+ }
2128
+
2129
+ // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
2130
+ hideLoading();
2131
+ $id('adminPage').style.display = 'block';
2132
+
2133
+ } catch (error) {
2134
+ console.error("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
2135
+ hideLoading();
2136
+ showError("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
2137
+ $id('home').style.display = 'block'; // ์˜ค๋ฅ˜ ์‹œ ํ™ˆ์œผ๋กœ ๋ณต๊ท€
2138
+ }
2139
+ }
2140
 
2141
  /* -- ๋กœ๋”ฉ ๋ฐ ์˜ค๋ฅ˜ ํ‘œ์‹œ -- */
2142
  function showLoading(message, progress = -1) {
 
2225
  }
2226
  }, 5000);
2227
  }
2228
+
2229
+ function showMessage(message) {
2230
+ // ๊ธฐ์กด ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
2231
+ const existingMessage = $id('messageContainer');
2232
+ if (existingMessage) {
2233
+ existingMessage.remove();
2234
+ }
2235
 
2236
+ const messageContainer = document.createElement('div');
2237
+ messageContainer.className = 'loading-container fade-in';
2238
+ messageContainer.id = 'messageContainer';
2239
+ messageContainer.innerHTML = `
2240
+ <p class="loading-text" style="color: #2ecc71;">${message}</p>
2241
+ <button id="messageCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">ํ™•์ธ</button>
2242
+ `;
2243
+
2244
+ document.body.appendChild(messageContainer);
2245
+
2246
+ // ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
2247
+ $id('messageCloseBtn').onclick = () => {
2248
+ messageContainer.remove();
2249
+ };
2250
+
2251
+ // 3์ดˆ ํ›„ ์ž๋™์œผ๋กœ ๋‹ซ๊ธฐ
2252
+ setTimeout(() => {
2253
+ if ($id('messageContainer')) {
2254
+ $id('messageContainer').remove();
2255
+ }
2256
+ }, 3000);
2257
+ }
2258
  </script>
2259
  </body>
2260
  </html>