ginipick commited on
Commit
5521c22
ยท
verified ยท
1 Parent(s): d4f44df

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +514 -261
app.py CHANGED
@@ -5,6 +5,8 @@ import pathlib, os, uvicorn, base64, json
5
  from typing import Dict, List, Any
6
  import asyncio
7
  import logging
 
 
8
 
9
  # ๋กœ๊น… ์„ค์ •
10
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
@@ -26,6 +28,8 @@ if not CACHE_DIR.exists():
26
 
27
  # ์ „์—ญ ์บ์‹œ ๊ฐ์ฒด
28
  pdf_cache: Dict[str, Dict[str, Any]] = {}
 
 
29
 
30
  # PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
31
  def get_pdf_files():
@@ -43,7 +47,7 @@ def generate_pdf_projects():
43
  projects_data.append({
44
  "path": str(pdf_file),
45
  "name": pdf_file.stem,
46
- "cached": pdf_file.stem in pdf_cache
47
  })
48
 
49
  return projects_data
@@ -52,7 +56,7 @@ def generate_pdf_projects():
52
  def get_cache_path(pdf_name: str):
53
  return CACHE_DIR / f"{pdf_name}_cache.json"
54
 
55
- # PDF ํŽ˜์ด์ง€ ์บ์‹ฑ ํ•จ์ˆ˜
56
  async def cache_pdf(pdf_path: str):
57
  try:
58
  import fitz # PyMuPDF
@@ -60,93 +64,144 @@ async def cache_pdf(pdf_path: str):
60
  pdf_file = pathlib.Path(pdf_path)
61
  pdf_name = pdf_file.stem
62
 
 
 
 
 
63
  # ์ด๋ฏธ ์บ์‹ฑ ์ค‘์ด๊ฑฐ๋‚˜ ์บ์‹ฑ ์™„๋ฃŒ๋œ PDF๋Š” ๊ฑด๋„ˆ๋›ฐ๊ธฐ
64
  if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
65
  logger.info(f"PDF {pdf_name} ์ด๋ฏธ ์บ์‹ฑ ์™„๋ฃŒ ๋˜๋Š” ์ง„ํ–‰ ์ค‘")
66
  return
67
 
68
- # ์บ์‹œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
69
- pdf_cache[pdf_name] = {"status": "processing", "progress": 0, "pages": []}
70
-
71
- # ์บ์‹œ ํŒŒ์ผ์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
72
- cache_path = get_cache_path(pdf_name)
73
- if cache_path.exists():
74
- try:
75
- with open(cache_path, "r") as cache_file:
76
- cached_data = json.load(cache_file)
77
- pdf_cache[pdf_name] = cached_data
78
- pdf_cache[pdf_name]["status"] = "completed"
79
- logger.info(f"์บ์‹œ ํŒŒ์ผ์—์„œ {pdf_name} ๋กœ๋“œ ์™„๋ฃŒ")
80
- return
81
- except Exception as e:
82
- logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์‹คํŒจ: {e}")
83
- # ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ ์ƒˆ๋กœ ์บ์‹ฑ
84
-
85
- # PDF ํŒŒ์ผ ์—ด๊ธฐ
86
- doc = fitz.open(pdf_path)
87
- total_pages = doc.page_count
88
-
89
- # ๊ฐ ํŽ˜์ด์ง€๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ์บ์‹ฑ
90
- pages = []
91
- for page_num in range(total_pages):
92
- # ์ง„ํ–‰ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
93
- progress = round((page_num + 1) / total_pages * 100)
94
- pdf_cache[pdf_name]["progress"] = progress
95
 
96
- # ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
97
- page = doc[page_num]
98
- pix = page.get_pixmap() # ๊ธฐ๋ณธ ํ•ด์ƒ๋„๋กœ ๋ Œ๋”๋ง
99
- img_data = pix.tobytes("png")
100
 
101
- # Base64 ์ธ์ฝ”๋”ฉ
102
- b64_img = base64.b64encode(img_data).decode('utf-8')
103
- img_src = f"data:image/png;base64,{b64_img}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- # ์ธ๋„ค์ผ์šฉ ์ž‘์€ ์ด๋ฏธ์ง€ (์ฒซ ํŽ˜์ด์ง€๋งŒ)
106
- if page_num == 0:
107
- pix_thumb = page.get_pixmap(matrix=fitz.Matrix(0.3, 0.3)) # ๋” ์ž‘์€ ์ธ๋„ค์ผ
 
 
108
  thumb_data = pix_thumb.tobytes("png")
109
  b64_thumb = base64.b64encode(thumb_data).decode('utf-8')
110
  thumb_src = f"data:image/png;base64,{b64_thumb}"
111
- else:
112
- thumb_src = "" # ์ฒซ ํŽ˜์ด์ง€ ์™ธ์—๋Š” ์ธ๋„ค์ผ ์ƒ๋žต
 
 
 
113
 
114
- # ํŽ˜์ด์ง€ ์ •๋ณด ์ €์žฅ
115
- pages.append({
116
- "src": img_src,
117
- "thumb": thumb_src if page_num == 0 else ""
118
- })
119
 
120
- # ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์ค‘๊ฐ„ ์ €์žฅ (20ํŽ˜์ด์ง€๋งˆ๋‹ค)
121
- if (page_num + 1) % 20 == 0 or page_num == total_pages - 1:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  pdf_cache[pdf_name]["pages"] = pages
123
- # ์บ์‹œ ํŒŒ์ผ ์ €์žฅ (์ง„ํ–‰ ์ค‘)
124
  try:
125
  with open(cache_path, "w") as cache_file:
126
  json.dump({
127
  "status": "processing",
128
- "progress": progress,
129
- "pages": pages,
130
  "total_pages": total_pages
131
  }, cache_file)
132
  except Exception as e:
133
  logger.error(f"์ค‘๊ฐ„ ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
134
-
135
- # ์บ์‹ฑ ์™„๋ฃŒ
136
- pdf_cache[pdf_name] = {
137
- "status": "completed",
138
- "progress": 100,
139
- "pages": pages,
140
- "total_pages": total_pages
141
- }
142
-
143
- # ์ตœ์ข… ์บ์‹œ ํŒŒ์ผ ์ €์žฅ
144
- try:
145
- with open(cache_path, "w") as cache_file:
146
- json.dump(pdf_cache[pdf_name], cache_file)
147
- logger.info(f"PDF {pdf_name} ์บ์‹ฑ ์™„๋ฃŒ, {total_pages}ํŽ˜์ด์ง€")
148
- except Exception as e:
149
- logger.error(f"์ตœ์ข… ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
150
 
151
  except Exception as e:
152
  import traceback
@@ -160,25 +215,24 @@ async def init_cache_all_pdfs():
160
  logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
161
  pdf_files = get_pdf_files()
162
 
163
- # ์ด๋ฏธ ์บ์‹œ๋œ PDF ํŒŒ์ผ ๋กœ๋“œ
164
  for cache_file in CACHE_DIR.glob("*_cache.json"):
165
  try:
166
  pdf_name = cache_file.stem.replace("_cache", "")
167
  with open(cache_file, "r") as f:
168
- pdf_cache[pdf_name] = json.load(f)
169
- pdf_cache[pdf_name]["status"] = "completed"
170
- logger.info(f"๊ธฐ์กด ์บ์‹œ ๋กœ๋“œ: {pdf_name}")
 
 
171
  except Exception as e:
172
  logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}")
173
 
174
- # ์บ์‹ฑ๋˜์ง€ ์•Š์€ PDF ํŒŒ์ผ ์ฒ˜๋ฆฌ
175
- for pdf_file in pdf_files:
176
- pdf_name = pdf_file.stem
177
- if pdf_name not in pdf_cache or pdf_cache[pdf_name].get("status") != "completed":
178
- try:
179
- await cache_pdf(str(pdf_file))
180
- except Exception as e:
181
- logger.error(f"PDF {pdf_name} ์บ์‹ฑ ์˜ค๋ฅ˜: {str(e)}")
182
 
183
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹œ์ž‘ ํ•จ์ˆ˜
184
  @app.on_event("startup")
@@ -191,7 +245,7 @@ async def startup_event():
191
  async def get_pdf_projects_api():
192
  return generate_pdf_projects()
193
 
194
- # API ์—”๋“œํฌ์ธํŠธ: PDF ์ธ๋„ค์ผ ์ œ๊ณต
195
  @app.get("/api/pdf-thumbnail")
196
  async def get_pdf_thumbnail(path: str):
197
  try:
@@ -199,23 +253,23 @@ async def get_pdf_thumbnail(path: str):
199
  pdf_name = pdf_file.stem
200
 
201
  # ์บ์‹œ์—์„œ ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
202
- if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed":
203
- if pdf_cache[pdf_name]["pages"] and pdf_cache[pdf_name]["pages"][0].get("thumb"):
204
  return {"thumbnail": pdf_cache[pdf_name]["pages"][0]["thumb"]}
205
-
206
- # ์บ์‹œ์— ์—†์œผ๋ฉด ์ƒ์„ฑ
207
  import fitz
208
  doc = fitz.open(path)
209
  if doc.page_count > 0:
210
  page = doc[0]
211
- pix = page.get_pixmap(matrix=fitz.Matrix(0.5, 0.5))
212
- img_data = pix.tobytes("png")
213
  b64_img = base64.b64encode(img_data).decode('utf-8')
214
 
215
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
216
  asyncio.create_task(cache_pdf(path))
217
 
218
- return {"thumbnail": f"data:image/png;base64,{b64_img}"}
219
 
220
  return {"thumbnail": None}
221
  except Exception as e:
@@ -235,7 +289,7 @@ async def get_cache_status(path: str = None):
235
  return {name: {"status": info["status"], "progress": info.get("progress", 0)}
236
  for name, info in pdf_cache.items()}
237
 
238
- # API ์—”๋“œํฌ์ธํŠธ: ์บ์‹œ๋œ PDF ์ฝ˜ํ…์ธ  ์ œ๊ณต
239
  @app.get("/api/cached-pdf")
240
  async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
241
  try:
@@ -243,12 +297,27 @@ async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
243
  pdf_name = pdf_file.stem
244
 
245
  # ์บ์‹œ ํ™•์ธ
246
- if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed":
247
- return pdf_cache[pdf_name]
248
-
249
- # ์บ๏ฟฝ๏ฟฝ๊ฐ€ ์ง„ํ–‰์ค‘์ธ ๊ฒฝ์šฐ
250
- if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "processing":
251
- return {"status": "processing", "progress": pdf_cache[pdf_name].get("progress", 0)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
  # ์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
254
  background_tasks.add_task(cache_pdf, path)
@@ -270,7 +339,9 @@ async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
270
  pdf_name = pdf_file.stem
271
 
272
  # ์บ์‹œ๋œ ๊ฒฝ์šฐ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
273
- if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed":
 
 
274
  return JSONResponse(content={"redirect": f"/api/cached-pdf?path={path}"})
275
 
276
  # ํŒŒ์ผ ์ฝ๊ธฐ
@@ -306,7 +377,7 @@ def get_html_content():
306
  return f.read()
307
  return HTML # ๊ธฐ๋ณธ HTML ์‚ฌ์šฉ
308
 
309
- # HTML ๋ฌธ์ž์—ด (์ด์ „์— ์—…๋ฐ์ดํŠธํ•œ HTML ๋‚ด์šฉ)
310
  HTML = """
311
  <!doctype html>
312
  <html lang="ko">
@@ -353,56 +424,69 @@ HTML = """
353
  background-attachment: fixed;
354
  }
355
 
356
- header {
357
- max-width: 1200px;
358
- margin: 0 auto;
359
- padding: 24px 30px;
 
 
 
 
 
 
 
 
360
  display: flex;
 
361
  align-items: center;
362
- background: rgba(255, 255, 255, 0.8);
363
- backdrop-filter: blur(8px);
364
- border-radius: 0 0 var(--radius-md) var(--radius-md);
365
- box-shadow: var(--shadow-sm);
366
- position: relative;
367
- z-index: 10;
368
- }
369
-
370
- #homeBtn {
371
- display: none;
372
- width: 48px;
373
- height: 48px;
374
- border: none;
375
- border-radius: 50%;
376
  cursor: pointer;
377
- background: var(--primary-color);
378
- color: white;
379
- font-size: 22px;
380
- margin-right: 16px;
381
- box-shadow: var(--shadow-sm);
382
  transition: var(--transition);
 
383
  }
384
-
385
- #homeBtn:hover {
386
- background: #8bc5f8;
387
  transform: scale(1.05);
388
- box-shadow: var(--shadow-md);
389
  }
390
-
391
- h2 {
392
- margin: 0;
393
- font-size: 1.75rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  font-weight: 600;
395
- background: linear-gradient(120deg, #667eea 0%, #764ba2 100%);
396
- -webkit-background-clip: text;
397
- background-clip: text;
398
- color: transparent;
399
- letter-spacing: 0.5px;
 
 
 
 
 
 
400
  }
401
 
402
  #home, #viewerPage {
 
403
  max-width: 1200px;
404
  margin: 0 auto;
405
- padding: 24px 30px 60px;
 
 
406
  position: relative;
407
  }
408
 
@@ -711,6 +795,49 @@ HTML = """
711
  transition: width 0.3s ease;
712
  }
713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
715
  @media (max-width: 768px) {
716
  .grid {
@@ -722,21 +849,33 @@ HTML = """
722
  height: 240px;
723
  }
724
 
725
- header {
726
- padding: 20px;
 
727
  }
728
 
729
- h2 {
730
- font-size: 1.5rem;
 
 
 
 
 
731
  }
732
  }
733
  </style>
734
  </head>
735
  <body>
736
- <header>
737
- <button id="homeBtn" title="ํ™ˆ์œผ๋กœ"><i class="fas fa-home"></i></button>
738
- <h2>My FlipBook Library</h2>
739
- </header>
 
 
 
 
 
 
740
 
741
  <section id="home" class="fade-in">
742
  <div class="upload-container">
@@ -761,6 +900,7 @@ HTML = """
761
 
762
  <section id="viewerPage" style="display:none">
763
  <div id="viewer"></div>
 
764
  </section>
765
 
766
  <script>
@@ -770,6 +910,10 @@ HTML = """
770
 
771
  // ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
772
  let serverProjects = [];
 
 
 
 
773
 
774
  /* ๐Ÿ”Š ์˜ค๋””์˜ค unlock โ€“ ๋‚ด์žฅ Audio ์™€ ๊ฐ™์€ MP3 ๊ฒฝ๋กœ ์‚ฌ์šฉ */
775
  ['click','touchstart'].forEach(evt=>{
@@ -798,7 +942,7 @@ HTML = """
798
  d.innerHTML = `
799
  <div class="card-inner">
800
  ${cachedBadge}
801
- <img src="${thumb}" alt="${displayTitle}">
802
  <p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
803
  </div>
804
  `;
@@ -884,29 +1028,46 @@ HTML = """
884
  return;
885
  }
886
 
887
- // ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์ธ๋„ค์ผ ์ƒ์„ฑ
888
- for(let i = 0; i < serverProjects.length; i++) {
889
- updateLoading(`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (${i+1}/${serverProjects.length})`);
890
 
891
- const project = serverProjects[i];
892
  const pdfName = project.name;
893
  const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
894
 
895
- // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
896
- const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`);
897
- const data = await response.json();
898
-
899
- if(data.thumbnail) {
900
- const pages = [{
901
- src: data.thumbnail,
902
- thumb: data.thumbnail,
903
- path: project.path,
904
- cached: isCached
905
- }];
906
 
907
- save(pages, project.name, isCached);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
  }
909
- }
 
 
 
 
 
 
 
 
 
 
910
 
911
  hideLoading();
912
 
@@ -944,6 +1105,9 @@ HTML = """
944
  badgeEl.className = 'cached-status';
945
  badgeEl.textContent = '์บ์‹œ๋จ';
946
  cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
 
 
 
947
  }
948
  projects[i][0].cached = true;
949
  } else if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "processing") {
@@ -958,6 +1122,30 @@ HTML = """
958
  }
959
  }
960
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
961
  } catch(error) {
962
  console.error('์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
963
  }
@@ -978,6 +1166,10 @@ HTML = """
978
  if(pages[0].path) {
979
  const pdfPath = pages[0].path;
980
 
 
 
 
 
981
  // ์บ์‹œ ์—ฌ๋ถ€ ํ™•์ธ
982
  if(pages[0].cached) {
983
  // ์บ์‹œ๋œ PDF ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
@@ -990,7 +1182,16 @@ HTML = """
990
  if(cachedData.status === "completed" && cachedData.pages) {
991
  hideLoading();
992
  createFlipBook(cachedData.pages);
 
993
  return;
 
 
 
 
 
 
 
 
994
  }
995
  } catch(error) {
996
  console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
@@ -998,109 +1199,157 @@ HTML = """
998
  }
999
  }
1000
 
1001
- // ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
1002
- showLoading("PDF ์ค€๋น„ ์ค‘...");
1003
-
1004
- try {
1005
- const response = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1006
- const data = await response.json();
1007
 
1008
- // ์บ์‹œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋œ ๊ฒฝ์šฐ
1009
- if(data.redirect) {
1010
- const redirectRes = await fetch(data.redirect);
1011
- const cachedData = await redirectRes.json();
1012
 
1013
- if(cachedData.status === "completed" && cachedData.pages) {
1014
- hideLoading();
1015
- createFlipBook(cachedData.pages);
1016
- return;
1017
- } else if(cachedData.status === "processing") {
1018
- // ์บ์‹ฑ ์ง„ํ–‰ ์ค‘์ธ ๊ฒฝ์šฐ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ
1019
- updateLoading(`PDF ์บ์‹ฑ ์ค‘: ${cachedData.progress}%`, cachedData.progress);
1020
 
1021
- // ์บ์‹ฑ ์™„๋ฃŒ ํ™•์ธ์„ ์œ„ํ•œ ํด๋ง
1022
- await waitForCaching(pdfPath);
1023
- return;
 
 
 
 
 
 
 
 
 
 
 
1024
  }
1025
- }
1026
-
1027
- // ์›๋ณธ PDF ๋กœ๋“œ (ArrayBuffer ํ˜•ํƒœ)
1028
- const pdfResponse = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1029
- const pdfData = await pdfResponse.arrayBuffer();
1030
-
1031
- // PDF ๋กœ๋“œ ๋ฐ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
1032
- const pdf = await pdfjsLib.getDocument({data: pdfData}).promise;
1033
- const pdfPages = [];
1034
-
1035
- for(let p = 1; p <= pdf.numPages; p++) {
1036
- updateLoading(`ํŽ˜์ด์ง€ ์ค€๋น„ ์ค‘... (${p}/${pdf.numPages})`);
1037
 
1038
- const pg = await pdf.getPage(p);
1039
- const vp = pg.getViewport({scale: 1});
1040
- const c = document.createElement('canvas');
1041
- c.width = vp.width;
1042
- c.height = vp.height;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
 
1044
- await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
1045
- pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1046
  }
1047
-
1048
- hideLoading();
1049
- createFlipBook(pdfPages);
1050
-
1051
- } catch(error) {
1052
- console.error('PDF ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
1053
- hideLoading();
1054
- showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1055
  }
1056
  } else {
1057
  // ๋กœ์ปฌ ์—…๋กœ๋“œ๋œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰
1058
  createFlipBook(pages);
 
1059
  }
1060
  }
1061
 
1062
- /* โ”€โ”€ ์บ์‹ฑ ์™„๋ฃŒ ๋Œ€๊ธฐ ํ•จ์ˆ˜ โ”€โ”€ */
1063
- async function waitForCaching(pdfPath) {
1064
- let retries = 0;
1065
- const maxRetries = 100; // ์ตœ๋Œ€ ํ™•์ธ ํšŸ์ˆ˜
 
 
 
 
 
 
1066
 
1067
- const checkInterval = setInterval(async () => {
 
 
 
 
 
 
 
1068
  try {
1069
- const response = await fetch(`/api/cache-status?path=${encodeURIComponent(pdfPath)}`);
1070
  const status = await response.json();
1071
 
1072
- if(status.status === "completed") {
1073
- // ์บ์‹ฑ ์™„๋ฃŒ
1074
- clearInterval(checkInterval);
1075
- hideLoading();
1076
-
1077
- // ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋กœ FlipBook ์ƒ์„ฑ
1078
- const cachedRes = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
1079
- const cachedData = await cachedRes.json();
1080
- createFlipBook(cachedData.pages);
1081
-
1082
- } else if(status.status === "processing") {
1083
- // ์ง„ํ–‰ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1084
- updateLoading(`PDF ์บ์‹ฑ ์ค‘: ${status.progress}%`, status.progress);
1085
- } else if(status.status === "error" || retries >= maxRetries) {
1086
- // ์˜ค๋ฅ˜ ๋˜๋Š” ์‹œ๊ฐ„ ์ดˆ๊ณผ
1087
- clearInterval(checkInterval);
1088
- hideLoading();
1089
- showError("PDF ์บ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
1090
  }
1091
-
1092
- retries++;
1093
- } catch(e) {
1094
  console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
1095
- retries++;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096
 
1097
- if(retries >= maxRetries) {
1098
- clearInterval(checkInterval);
1099
- hideLoading();
1100
- showError("PDF ์บ์‹ฑ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
1101
- }
1102
  }
1103
- }, 1000); // 1์ดˆ๋งˆ๋‹ค ํ™•์ธ
 
 
1104
  }
1105
 
1106
  function createFlipBook(pages) {
@@ -1143,8 +1392,20 @@ HTML = """
1143
  viewer.style.width = size.width + 'px';
1144
  viewer.style.height = size.height + 'px';
1145
 
 
 
 
 
 
 
 
 
 
 
 
 
1146
  fb = new FlipBook(viewer, {
1147
- pages: pages,
1148
  viewMode: 'webgl',
1149
  autoSize: true,
1150
  flipDuration: 800,
@@ -1215,41 +1476,33 @@ HTML = """
1215
  }
1216
 
1217
  /* โ”€โ”€ ๋„ค๋น„๊ฒŒ์ด์…˜ โ”€โ”€ */
1218
- $id('homeBtn').onclick=()=>{
1219
  if(fb) {
1220
  fb.destroy();
1221
  viewer.innerHTML = '';
1222
  fb = null;
1223
  }
1224
  toggle(true);
 
 
 
 
 
 
 
 
1225
  };
1226
 
1227
  function toggle(showHome){
1228
  $id('home').style.display=showHome?'block':'none';
1229
  $id('viewerPage').style.display=showHome?'none':'block';
1230
- $id('homeBtn').style.display=showHome?'none':'inline-block';
1231
 
1232
  // ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
1233
  if(!showHome) {
1234
  document.body.classList.add('viewer-mode');
1235
- $id('homeBtn').style.position = 'fixed';
1236
- $id('homeBtn').style.top = '20px';
1237
- $id('homeBtn').style.left = '20px';
1238
- $id('homeBtn').style.zIndex = '9999';
1239
- $id('homeBtn').style.fontSize = '';
1240
- $id('homeBtn').style.width = '52px';
1241
- $id('homeBtn').style.height = '52px';
1242
- $id('homeBtn').style.boxShadow = '0 4px 15px rgba(0,0,0,0.15)';
1243
  } else {
1244
  document.body.classList.remove('viewer-mode');
1245
- $id('homeBtn').style.position = '';
1246
- $id('homeBtn').style.top = '';
1247
- $id('homeBtn').style.left = '';
1248
- $id('homeBtn').style.zIndex = '';
1249
- $id('homeBtn').style.fontSize = '';
1250
- $id('homeBtn').style.width = '';
1251
- $id('homeBtn').style.height = '';
1252
- $id('homeBtn').style.boxShadow = '';
1253
  }
1254
  }
1255
 
@@ -1345,8 +1598,8 @@ HTML = """
1345
  window.addEventListener('DOMContentLoaded', () => {
1346
  loadServerPDFs();
1347
 
1348
- // ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ (5์ดˆ๋งˆ๋‹ค)
1349
- setInterval(checkCacheStatus, 5000);
1350
  });
1351
  </script>
1352
  </body>
 
5
  from typing import Dict, List, Any
6
  import asyncio
7
  import logging
8
+ import threading
9
+ import concurrent.futures
10
 
11
  # ๋กœ๊น… ์„ค์ •
12
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
28
 
29
  # ์ „์—ญ ์บ์‹œ ๊ฐ์ฒด
30
  pdf_cache: Dict[str, Dict[str, Any]] = {}
31
+ # ์บ์‹ฑ ๋ฝ
32
+ cache_locks = {}
33
 
34
  # PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
35
  def get_pdf_files():
 
47
  projects_data.append({
48
  "path": str(pdf_file),
49
  "name": pdf_file.stem,
50
+ "cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
51
  })
52
 
53
  return projects_data
 
56
  def get_cache_path(pdf_name: str):
57
  return CACHE_DIR / f"{pdf_name}_cache.json"
58
 
59
+ # ์ตœ์ ํ™”๋œ PDF ํŽ˜์ด์ง€ ์บ์‹ฑ ํ•จ์ˆ˜
60
  async def cache_pdf(pdf_path: str):
61
  try:
62
  import fitz # PyMuPDF
 
64
  pdf_file = pathlib.Path(pdf_path)
65
  pdf_name = pdf_file.stem
66
 
67
+ # ๋ฝ ์ƒ์„ฑ - ๋™์ผํ•œ PDF์— ๋Œ€ํ•ด ๋™์‹œ ์บ์‹ฑ ๋ฐฉ์ง€
68
+ if pdf_name not in cache_locks:
69
+ cache_locks[pdf_name] = threading.Lock()
70
+
71
  # ์ด๋ฏธ ์บ์‹ฑ ์ค‘์ด๊ฑฐ๋‚˜ ์บ์‹ฑ ์™„๋ฃŒ๋œ PDF๋Š” ๊ฑด๋„ˆ๋›ฐ๊ธฐ
72
  if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
73
  logger.info(f"PDF {pdf_name} ์ด๋ฏธ ์บ์‹ฑ ์™„๋ฃŒ ๋˜๋Š” ์ง„ํ–‰ ์ค‘")
74
  return
75
 
76
+ with cache_locks[pdf_name]:
77
+ # ์ด์ค‘ ์ฒดํฌ - ๋ฝ ํš๋“ ํ›„ ๋‹ค์‹œ ํ™•์ธ
78
+ if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
79
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ # ์บ์‹œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
82
+ pdf_cache[pdf_name] = {"status": "processing", "progress": 0, "pages": []}
 
 
83
 
84
+ # ์บ์‹œ ํŒŒ์ผ์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
85
+ cache_path = get_cache_path(pdf_name)
86
+ if cache_path.exists():
87
+ try:
88
+ with open(cache_path, "r") as cache_file:
89
+ cached_data = json.load(cache_file)
90
+ if cached_data.get("status") == "completed" and cached_data.get("pages"):
91
+ pdf_cache[pdf_name] = cached_data
92
+ pdf_cache[pdf_name]["status"] = "completed"
93
+ logger.info(f"์บ์‹œ ํŒŒ์ผ์—์„œ {pdf_name} ๋กœ๋“œ ์™„๋ฃŒ")
94
+ return
95
+ except Exception as e:
96
+ logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์‹คํŒจ: {e}")
97
+
98
+ # PDF ํŒŒ์ผ ์—ด๊ธฐ
99
+ doc = fitz.open(pdf_path)
100
+ total_pages = doc.page_count
101
 
102
+ # ๋ฏธ๋ฆฌ ์ธ๋„ค์ผ๋งŒ ๋จผ์ € ์ƒ์„ฑ (๋น ๋ฅธ UI ๋กœ๋”ฉ์šฉ)
103
+ if total_pages > 0:
104
+ # ์ฒซ ํŽ˜์ด์ง€ ์ธ๋„ค์ผ ์ƒ์„ฑ
105
+ page = doc[0]
106
+ pix_thumb = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2)) # ๋” ์ž‘์€ ์ธ๋„ค์ผ
107
  thumb_data = pix_thumb.tobytes("png")
108
  b64_thumb = base64.b64encode(thumb_data).decode('utf-8')
109
  thumb_src = f"data:image/png;base64,{b64_thumb}"
110
+
111
+ # ์ธ๋„ค์ผ ํŽ˜์ด์ง€๋งŒ ๋จผ์ € ์บ์‹œ
112
+ pdf_cache[pdf_name]["pages"] = [{"thumb": thumb_src, "src": ""}]
113
+ pdf_cache[pdf_name]["progress"] = 1
114
+ pdf_cache[pdf_name]["total_pages"] = total_pages
115
 
116
+ # ์ด๋ฏธ์ง€ ํ•ด์ƒ๋„ ๋ฐ ์••์ถ• ํ’ˆ์งˆ ์„ค์ • (์„ฑ๋Šฅ ์ตœ์ ํ™”)
117
+ scale_factor = 1.0 # ๊ธฐ๋ณธ ํ•ด์ƒ๋„ (๋‚ฎ์ถœ์ˆ˜๋ก ๋กœ๋”ฉ ๋น ๋ฆ„)
118
+ jpeg_quality = 80 # JPEG ํ’ˆ์งˆ (๋‚ฎ์ถœ์ˆ˜๋ก ์šฉ๋Ÿ‰ ์ž‘์•„์ง)
 
 
119
 
120
+ # ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ ์ž‘์—…์ž ํ•จ์ˆ˜ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ์šฉ)
121
+ def process_page(page_num):
122
+ try:
123
+ page = doc[page_num]
124
+
125
+ # ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ ์‹œ ๋งคํŠธ๋ฆญ์Šค ์Šค์ผ€์ผ๋ง ์ ์šฉ (์„ฑ๋Šฅ ์ตœ์ ํ™”)
126
+ pix = page.get_pixmap(matrix=fitz.Matrix(scale_factor, scale_factor))
127
+
128
+ # JPEG ํ˜•์‹์œผ๋กœ ์ธ์ฝ”๋”ฉ (PNG๋ณด๋‹ค ํฌ๊ธฐ ์ž‘์Œ)
129
+ img_data = pix.tobytes("jpeg", jpeg_quality)
130
+ b64_img = base64.b64encode(img_data).decode('utf-8')
131
+ img_src = f"data:image/jpeg;base64,{b64_img}"
132
+
133
+ # ์ธ๋„ค์ผ (์ฒซ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹ˆ๋ฉด ๋นˆ ๋ฌธ์ž์—ด)
134
+ thumb_src = "" if page_num > 0 else pdf_cache[pdf_name]["pages"][0]["thumb"]
135
+
136
+ return {
137
+ "page_num": page_num,
138
+ "src": img_src,
139
+ "thumb": thumb_src
140
+ }
141
+ except Exception as e:
142
+ logger.error(f"ํŽ˜์ด์ง€ {page_num} ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
143
+ return {
144
+ "page_num": page_num,
145
+ "src": "",
146
+ "thumb": "",
147
+ "error": str(e)
148
+ }
149
+
150
+ # ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ๋ชจ๋“  ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ
151
+ pages = [None] * total_pages
152
+ processed_count = 0
153
+
154
+ # ํŽ˜์ด์ง€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ)
155
+ batch_size = 5 # ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ํŽ˜์ด์ง€ ์ˆ˜
156
+
157
+ for batch_start in range(0, total_pages, batch_size):
158
+ batch_end = min(batch_start + batch_size, total_pages)
159
+ current_batch = list(range(batch_start, batch_end))
160
+
161
+ # ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ๋ฐฐ์น˜ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
162
+ with concurrent.futures.ThreadPoolExecutor(max_workers=min(5, batch_size)) as executor:
163
+ batch_results = list(executor.map(process_page, current_batch))
164
+
165
+ # ๊ฒฐ๊ณผ ์ €์žฅ
166
+ for result in batch_results:
167
+ page_num = result["page_num"]
168
+ pages[page_num] = {
169
+ "src": result["src"],
170
+ "thumb": result["thumb"]
171
+ }
172
+
173
+ processed_count += 1
174
+ progress = round(processed_count / total_pages * 100)
175
+ pdf_cache[pdf_name]["progress"] = progress
176
+
177
+ # ์ค‘๊ฐ„ ์ €์žฅ
178
  pdf_cache[pdf_name]["pages"] = pages
 
179
  try:
180
  with open(cache_path, "w") as cache_file:
181
  json.dump({
182
  "status": "processing",
183
+ "progress": pdf_cache[pdf_name]["progress"],
184
+ "pages": pdf_cache[pdf_name]["pages"],
185
  "total_pages": total_pages
186
  }, cache_file)
187
  except Exception as e:
188
  logger.error(f"์ค‘๊ฐ„ ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
189
+
190
+ # ์บ์‹ฑ ์™„๋ฃŒ
191
+ pdf_cache[pdf_name] = {
192
+ "status": "completed",
193
+ "progress": 100,
194
+ "pages": pages,
195
+ "total_pages": total_pages
196
+ }
197
+
198
+ # ์ตœ์ข… ์บ์‹œ ํŒŒ์ผ ์ €์žฅ
199
+ try:
200
+ with open(cache_path, "w") as cache_file:
201
+ json.dump(pdf_cache[pdf_name], cache_file)
202
+ logger.info(f"PDF {pdf_name} ์บ์‹ฑ ์™„๋ฃŒ, {total_pages}ํŽ˜์ด์ง€")
203
+ except Exception as e:
204
+ logger.error(f"์ตœ์ข… ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
205
 
206
  except Exception as e:
207
  import traceback
 
215
  logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
216
  pdf_files = get_pdf_files()
217
 
218
+ # ์ด๋ฏธ ์บ์‹œ๋œ PDF ํŒŒ์ผ ๋กœ๋“œ (๋น ๋ฅธ ์‹œ์ž‘์„ ์œ„ํ•ด ๋จผ์ € ์ˆ˜ํ–‰)
219
  for cache_file in CACHE_DIR.glob("*_cache.json"):
220
  try:
221
  pdf_name = cache_file.stem.replace("_cache", "")
222
  with open(cache_file, "r") as f:
223
+ cached_data = json.load(f)
224
+ if cached_data.get("status") == "completed" and cached_data.get("pages"):
225
+ pdf_cache[pdf_name] = cached_data
226
+ pdf_cache[pdf_name]["status"] = "completed"
227
+ logger.info(f"๊ธฐ์กด ์บ์‹œ ๋กœ๋“œ: {pdf_name}")
228
  except Exception as e:
229
  logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}")
230
 
231
+ # ์บ์‹ฑ๋˜์ง€ ์•Š์€ PDF ํŒŒ์ผ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
232
+ await asyncio.gather(*[asyncio.create_task(cache_pdf(str(pdf_file)))
233
+ for pdf_file in pdf_files
234
+ if pdf_file.stem not in pdf_cache
235
+ or pdf_cache[pdf_file.stem].get("status") != "completed"])
 
 
 
236
 
237
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹œ์ž‘ ํ•จ์ˆ˜
238
  @app.on_event("startup")
 
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):
251
  try:
 
253
  pdf_name = pdf_file.stem
254
 
255
  # ์บ์‹œ์—์„œ ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
256
+ if pdf_name in pdf_cache and pdf_cache[pdf_name].get("pages"):
257
+ if pdf_cache[pdf_name]["pages"][0].get("thumb"):
258
  return {"thumbnail": pdf_cache[pdf_name]["pages"][0]["thumb"]}
259
+
260
+ # ์บ์‹œ์— ์—†์œผ๋ฉด ์ƒ์„ฑ (๋” ์ž‘๊ณ  ๋น ๋ฅธ ์ธ๋„ค์ผ)
261
  import fitz
262
  doc = fitz.open(path)
263
  if doc.page_count > 0:
264
  page = doc[0]
265
+ pix = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2)) # ๋” ์ž‘์€ ์ธ๋„ค์ผ
266
+ img_data = pix.tobytes("jpeg", 70) # JPEG ์••์ถ• ์‚ฌ์šฉ
267
  b64_img = base64.b64encode(img_data).decode('utf-8')
268
 
269
  # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
270
  asyncio.create_task(cache_pdf(path))
271
 
272
+ return {"thumbnail": f"data:image/jpeg;base64,{b64_img}"}
273
 
274
  return {"thumbnail": None}
275
  except Exception as e:
 
289
  return {name: {"status": info["status"], "progress": info.get("progress", 0)}
290
  for name, info in pdf_cache.items()}
291
 
292
+ # API ์—”๋“œํฌ์ธํŠธ: ์บ์‹œ๋œ PDF ์ฝ˜ํ…์ธ  ์ œ๊ณต (์ ์ง„์  ๋กœ๋”ฉ ์ง€์›)
293
  @app.get("/api/cached-pdf")
294
  async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
295
  try:
 
297
  pdf_name = pdf_file.stem
298
 
299
  # ์บ์‹œ ํ™•์ธ
300
+ if pdf_name in pdf_cache:
301
+ status = pdf_cache[pdf_name].get("status", "")
302
+
303
+ # ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
304
+ if status == "completed":
305
+ return pdf_cache[pdf_name]
306
+
307
+ # ์ฒ˜๋ฆฌ ์ค‘์ธ ๊ฒฝ์šฐ ํ˜„์žฌ๊นŒ์ง€์˜ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ํฌํ•จ (์ ์ง„์  ๋กœ๋”ฉ)
308
+ elif status == "processing":
309
+ progress = pdf_cache[pdf_name].get("progress", 0)
310
+ pages = pdf_cache[pdf_name].get("pages", [])
311
+ total_pages = pdf_cache[pdf_name].get("total_pages", 0)
312
+
313
+ # ์ผ๋ถ€๋งŒ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ์—๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํŽ˜์ด์ง€ ์ œ๊ณต
314
+ return {
315
+ "status": "processing",
316
+ "progress": progress,
317
+ "pages": pages,
318
+ "total_pages": total_pages,
319
+ "available_pages": len([p for p in pages if p and p.get("src")])
320
+ }
321
 
322
  # ์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
323
  background_tasks.add_task(cache_pdf, path)
 
339
  pdf_name = pdf_file.stem
340
 
341
  # ์บ์‹œ๋œ ๊ฒฝ์šฐ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
342
+ if pdf_name in pdf_cache and (pdf_cache[pdf_name].get("status") == "completed"
343
+ or (pdf_cache[pdf_name].get("status") == "processing"
344
+ and pdf_cache[pdf_name].get("progress", 0) > 10)):
345
  return JSONResponse(content={"redirect": f"/api/cached-pdf?path={path}"})
346
 
347
  # ํŒŒ์ผ ์ฝ๊ธฐ
 
377
  return f.read()
378
  return HTML # ๊ธฐ๋ณธ HTML ์‚ฌ์šฉ
379
 
380
+ # HTML ๋ฌธ์ž์—ด (UI ์ˆ˜์ • ๋ฒ„์ „)
381
  HTML = """
382
  <!doctype html>
383
  <html lang="ko">
 
424
  background-attachment: fixed;
425
  }
426
 
427
+ /* ํ—ค๋” ์ œ๋ชฉ ์ œ๊ฑฐ ๋ฐ Home ๋ฒ„ํŠผ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ */
428
+ .floating-home {
429
+ position: fixed;
430
+ top: 20px;
431
+ left: 20px;
432
+ width: 60px;
433
+ height: 60px;
434
+ border-radius: 50%;
435
+ background: rgba(255, 255, 255, 0.9);
436
+ backdrop-filter: blur(10px);
437
+ box-shadow: var(--shadow-md);
438
+ z-index: 9999;
439
  display: flex;
440
+ justify-content: center;
441
  align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  cursor: pointer;
 
 
 
 
 
443
  transition: var(--transition);
444
+ overflow: hidden;
445
  }
446
+
447
+ .floating-home:hover {
 
448
  transform: scale(1.05);
449
+ box-shadow: var(--shadow-lg);
450
  }
451
+
452
+ .floating-home .icon {
453
+ font-size: 22px;
454
+ color: var(--primary-color);
455
+ transition: var(--transition);
456
+ }
457
+
458
+ .floating-home:hover .icon {
459
+ color: #8bc5f8;
460
+ }
461
+
462
+ .floating-home .title {
463
+ position: absolute;
464
+ left: 70px;
465
+ background: rgba(255, 255, 255, 0.95);
466
+ padding: 8px 20px;
467
+ border-radius: 20px;
468
+ box-shadow: var(--shadow-sm);
469
  font-weight: 600;
470
+ font-size: 14px;
471
+ white-space: nowrap;
472
+ pointer-events: none;
473
+ opacity: 0;
474
+ transform: translateX(-10px);
475
+ transition: all 0.3s ease;
476
+ }
477
+
478
+ .floating-home:hover .title {
479
+ opacity: 1;
480
+ transform: translateX(0);
481
  }
482
 
483
  #home, #viewerPage {
484
+ padding-top: 100px;
485
  max-width: 1200px;
486
  margin: 0 auto;
487
+ padding-bottom: 60px;
488
+ padding-left: 30px;
489
+ padding-right: 30px;
490
  position: relative;
491
  }
492
 
 
795
  transition: width 0.3s ease;
796
  }
797
 
798
+ /* ํ—ค๋” ๋กœ๊ณ  ๋ฐ ํƒ€์ดํ‹€ */
799
+ .library-header {
800
+ position: fixed;
801
+ top: 20px;
802
+ left: 0;
803
+ right: 0;
804
+ text-align: center;
805
+ z-index: 100;
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 {
827
+ position: absolute;
828
+ bottom: 20px;
829
+ left: 50%;
830
+ transform: translateX(-50%);
831
+ background: rgba(255, 255, 255, 0.9);
832
+ padding: 10px 20px;
833
+ border-radius: 20px;
834
+ box-shadow: var(--shadow-md);
835
+ font-size: 14px;
836
+ color: var(--text-color);
837
+ z-index: 9998;
838
+ text-align: center;
839
+ }
840
+
841
  /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
842
  @media (max-width: 768px) {
843
  .grid {
 
849
  height: 240px;
850
  }
851
 
852
+ .library-header .title {
853
+ font-size: 1.25rem;
854
+ padding: 10px 20px;
855
  }
856
 
857
+ .floating-home {
858
+ width: 50px;
859
+ height: 50px;
860
+ }
861
+
862
+ .floating-home .icon {
863
+ font-size: 18px;
864
  }
865
  }
866
  </style>
867
  </head>
868
  <body>
869
+ <!-- ์ œ๋ชฉ์„ Home ๋ฒ„ํŠผ๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์–ด๋กœ ์ฒ˜๋ฆฌ -->
870
+ <div id="homeButton" class="floating-home" style="display:none;">
871
+ <div class="icon"><i class="fas fa-home"></i></div>
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">
 
900
 
901
  <section id="viewerPage" style="display:none">
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>
 
910
 
911
  // ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
912
  let serverProjects = [];
913
+
914
+ // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ
915
+ let currentLoadingPdfPath = null;
916
+ let pageLoadingInterval = null;
917
 
918
  /* ๐Ÿ”Š ์˜ค๋””์˜ค unlock โ€“ ๋‚ด์žฅ Audio ์™€ ๊ฐ™์€ MP3 ๊ฒฝ๋กœ ์‚ฌ์šฉ */
919
  ['click','touchstart'].forEach(evt=>{
 
942
  d.innerHTML = `
943
  <div class="card-inner">
944
  ${cachedBadge}
945
+ <img src="${thumb}" alt="${displayTitle}" loading="lazy">
946
  <p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
947
  </div>
948
  `;
 
1028
  return;
1029
  }
1030
 
1031
+ // ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์ธ๋„ค์ผ ์ƒ์„ฑ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์ตœ์ ํ™”)
1032
+ const thumbnailPromises = serverProjects.map(async (project, index) => {
1033
+ updateLoading(`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (${index+1}/${serverProjects.length})`);
1034
 
 
1035
  const pdfName = project.name;
1036
  const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
1037
 
1038
+ try {
1039
+ // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
1040
+ const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`);
1041
+ const data = await response.json();
 
 
 
 
 
 
 
1042
 
1043
+ if(data.thumbnail) {
1044
+ const pages = [{
1045
+ src: data.thumbnail,
1046
+ thumb: data.thumbnail,
1047
+ path: project.path,
1048
+ cached: isCached
1049
+ }];
1050
+
1051
+ return {
1052
+ pages,
1053
+ name: project.name,
1054
+ isCached
1055
+ };
1056
+ }
1057
+ } catch (err) {
1058
+ console.error(`์ธ๋„ค์ผ ๋กœ๋“œ ์˜ค๋ฅ˜ (${project.name}):`, err);
1059
  }
1060
+
1061
+ return null;
1062
+ });
1063
+
1064
+ // ๋ชจ๋“  ์ธ๋„ค์ผ ์š”์ฒญ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ
1065
+ const results = await Promise.all(thumbnailPromises);
1066
+
1067
+ // ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ ๊ฒฐ๊ณผ๋งŒ ํ‘œ์‹œ
1068
+ results.filter(result => result !== null).forEach(result => {
1069
+ save(result.pages, result.name, result.isCached);
1070
+ });
1071
 
1072
  hideLoading();
1073
 
 
1105
  badgeEl.className = 'cached-status';
1106
  badgeEl.textContent = '์บ์‹œ๋จ';
1107
  cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1108
+ } else if (badgeEl.textContent !== '์บ์‹œ๋จ') {
1109
+ badgeEl.textContent = '์บ์‹œ๋จ';
1110
+ badgeEl.style.background = 'var(--accent-color)';
1111
  }
1112
  projects[i][0].cached = true;
1113
  } else if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "processing") {
 
1122
  }
1123
  }
1124
 
1125
+ // ํ˜„์žฌ ๋กœ๋”ฉ ์ค‘์ธ PDF๊ฐ€ ์žˆ์œผ๋ฉด ์ƒํƒœ ํ™•์ธ
1126
+ if (currentLoadingPdfPath && pageLoadingInterval) {
1127
+ const pdfName = currentLoadingPdfPath.split('/').pop().replace('.pdf', '');
1128
+
1129
+ if (cacheStatus[pdfName]) {
1130
+ const status = cacheStatus[pdfName].status;
1131
+ const progress = cacheStatus[pdfName].progress || 0;
1132
+
1133
+ if (status === "completed") {
1134
+ // ์บ์‹ฑ ์™„๋ฃŒ ์‹œ
1135
+ clearInterval(pageLoadingInterval);
1136
+ $id('loadingPages').style.display = 'none';
1137
+ currentLoadingPdfPath = null;
1138
+
1139
+ // ์™„๋ฃŒ๋œ ์บ์‹œ๋กœ ํ”Œ๋ฆฝ๋ถ ๋‹ค์‹œ ๋กœ๋“œ
1140
+ refreshFlipBook();
1141
+ } else if (status === "processing") {
1142
+ // ์ง„ํ–‰ ์ค‘์ผ ๋•Œ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
1143
+ $id('loadingPages').style.display = 'block';
1144
+ $id('loadingPagesCount').textContent = `${progress}%`;
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
  } catch(error) {
1150
  console.error('์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
1151
  }
 
1166
  if(pages[0].path) {
1167
  const pdfPath = pages[0].path;
1168
 
1169
+ // ์ ์ง„์  ๋กœ๋”ฉ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™”
1170
+ let progressiveLoading = false;
1171
+ currentLoadingPdfPath = pdfPath;
1172
+
1173
  // ์บ์‹œ ์—ฌ๋ถ€ ํ™•์ธ
1174
  if(pages[0].cached) {
1175
  // ์บ์‹œ๋œ PDF ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
 
1182
  if(cachedData.status === "completed" && cachedData.pages) {
1183
  hideLoading();
1184
  createFlipBook(cachedData.pages);
1185
+ currentLoadingPdfPath = null;
1186
  return;
1187
+ } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1188
+ // ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
1189
+ hideLoading();
1190
+ createFlipBook(cachedData.pages);
1191
+ progressiveLoading = true;
1192
+
1193
+ // ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
1194
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1195
  }
1196
  } catch(error) {
1197
  console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
 
1199
  }
1200
  }
1201
 
1202
+ if (!progressiveLoading) {
1203
+ // ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
1204
+ showLoading("PDF ์ค€๋น„ ์ค‘...");
 
 
 
1205
 
1206
+ try {
1207
+ const response = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1208
+ const data = await response.json();
 
1209
 
1210
+ // ์บ์‹œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋œ ๊ฒฝ์šฐ
1211
+ if(data.redirect) {
1212
+ const redirectRes = await fetch(data.redirect);
1213
+ const cachedData = await redirectRes.json();
 
 
 
1214
 
1215
+ if(cachedData.status === "completed" && cachedData.pages) {
1216
+ hideLoading();
1217
+ createFlipBook(cachedData.pages);
1218
+ currentLoadingPdfPath = null;
1219
+ return;
1220
+ } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1221
+ // ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
1222
+ hideLoading();
1223
+ createFlipBook(cachedData.pages);
1224
+
1225
+ // ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
1226
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1227
+ return;
1228
+ }
1229
  }
 
 
 
 
 
 
 
 
 
 
 
 
1230
 
1231
+ // ์›๋ณธ PDF ๋กœ๋“œ (ArrayBuffer ํ˜•ํƒœ)
1232
+ const pdfResponse = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1233
+
1234
+ // JSON ์‘๋‹ต์ธ ๊ฒฝ์šฐ (๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋“ฑ)
1235
+ try {
1236
+ const jsonData = await pdfResponse.clone().json();
1237
+ if (jsonData.redirect) {
1238
+ const redirectRes = await fetch(jsonData.redirect);
1239
+ const cachedData = await redirectRes.json();
1240
+
1241
+ if(cachedData.pages && cachedData.pages.length > 0) {
1242
+ hideLoading();
1243
+ createFlipBook(cachedData.pages);
1244
+
1245
+ if(cachedData.status === "processing") {
1246
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1247
+ } else {
1248
+ currentLoadingPdfPath = null;
1249
+ }
1250
+ return;
1251
+ }
1252
+ }
1253
+ } catch (e) {
1254
+ // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์›๋ณธ PDF ๋ฐ์ดํ„ฐ๋กœ ์ฒ˜๋ฆฌ
1255
+ }
1256
+
1257
+ // ArrayBuffer ํ˜•ํƒœ์˜ PDF ๋ฐ์ดํ„ฐ
1258
+ const pdfData = await pdfResponse.arrayBuffer();
1259
+
1260
+ // PDF ๋กœ๋“œ ๋ฐ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
1261
+ const pdf = await pdfjsLib.getDocument({data: pdfData}).promise;
1262
+ const pdfPages = [];
1263
 
1264
+ for(let p = 1; p <= pdf.numPages; p++) {
1265
+ updateLoading(`ํŽ˜์ด์ง€ ์ค€๋น„ ์ค‘... (${p}/${pdf.numPages})`);
1266
+
1267
+ const pg = await pdf.getPage(p);
1268
+ const vp = pg.getViewport({scale: 1});
1269
+ const c = document.createElement('canvas');
1270
+ c.width = vp.width;
1271
+ c.height = vp.height;
1272
+
1273
+ await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
1274
+ pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
1275
+ }
1276
+
1277
+ hideLoading();
1278
+ createFlipBook(pdfPages);
1279
+ currentLoadingPdfPath = null;
1280
+
1281
+ } catch(error) {
1282
+ console.error('PDF ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
1283
+ hideLoading();
1284
+ showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1285
+ currentLoadingPdfPath = null;
1286
  }
 
 
 
 
 
 
 
 
1287
  }
1288
  } else {
1289
  // ๋กœ์ปฌ ์—…๋กœ๋“œ๋œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰
1290
  createFlipBook(pages);
1291
+ currentLoadingPdfPath = null;
1292
  }
1293
  }
1294
 
1295
+ /* โ”€โ”€ ์ ์ง„์  ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์‹œ์ž‘ โ”€โ”€ */
1296
+ function startProgressiveLoadingIndicator(progress, totalPages) {
1297
+ // ์ง„ํ–‰ ์ƒํƒœ ํ‘œ์‹œ ํ™œ์„ฑํ™”
1298
+ $id('loadingPages').style.display = 'block';
1299
+ $id('loadingPagesCount').textContent = `${progress}%`;
1300
+
1301
+ // ๊ธฐ์กด ์ธํ„ฐ๋ฒŒ ์ œ๊ฑฐ
1302
+ if (pageLoadingInterval) {
1303
+ clearInterval(pageLoadingInterval);
1304
+ }
1305
 
1306
+ // ์ฃผ๊ธฐ์ ์œผ๋กœ ์บ์‹œ ์ƒํƒœ ํ™•์ธ (2์ดˆ๋งˆ๋‹ค)
1307
+ pageLoadingInterval = setInterval(async () => {
1308
+ if (!currentLoadingPdfPath) {
1309
+ clearInterval(pageLoadingInterval);
1310
+ $id('loadingPages').style.display = 'none';
1311
+ return;
1312
+ }
1313
+
1314
  try {
1315
+ const response = await fetch(`/api/cache-status?path=${encodeURIComponent(currentLoadingPdfPath)}`);
1316
  const status = await response.json();
1317
 
1318
+ // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1319
+ if (status.status === "completed") {
1320
+ clearInterval(pageLoadingInterval);
1321
+ $id('loadingPages').style.display = 'none';
1322
+ refreshFlipBook(); // ์™„๋ฃŒ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ๊ณ ์นจ
1323
+ currentLoadingPdfPath = null;
1324
+ } else if (status.status === "processing") {
1325
+ $id('loadingPagesCount').textContent = `${status.progress}%`;
 
 
 
 
 
 
 
 
 
 
1326
  }
1327
+ } catch (e) {
 
 
1328
  console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
1329
+ }
1330
+ }, 1000);
1331
+ }
1332
+
1333
+ /* โ”€โ”€ ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ โ”€โ”€ */
1334
+ async function refreshFlipBook() {
1335
+ if (!currentLoadingPdfPath || !fb) return;
1336
+
1337
+ try {
1338
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(currentLoadingPdfPath)}`);
1339
+ const cachedData = await response.json();
1340
+
1341
+ if(cachedData.status === "completed" && cachedData.pages) {
1342
+ // ๊ธฐ์กด ํ”Œ๋ฆฝ๋ถ ์ •๋ฆฌ
1343
+ fb.destroy();
1344
+ viewer.innerHTML = '';
1345
 
1346
+ // ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ์žฌ์ƒ์„ฑ
1347
+ createFlipBook(cachedData.pages);
1348
+ currentLoadingPdfPath = null;
 
 
1349
  }
1350
+ } catch (e) {
1351
+ console.error("ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜:", e);
1352
+ }
1353
  }
1354
 
1355
  function createFlipBook(pages) {
 
1392
  viewer.style.width = size.width + 'px';
1393
  viewer.style.height = size.height + 'px';
1394
 
1395
+ // ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ •์ œ (๋นˆ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ)
1396
+ const validPages = pages.map(page => {
1397
+ // src๊ฐ€ ์—†๋Š” ํŽ˜์ด์ง€๋Š” ๋กœ๋”ฉ ์ค‘ ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด
1398
+ if (!page || !page.src) {
1399
+ return {
1400
+ src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+',
1401
+ thumb: page && page.thumb ? page.thumb : ''
1402
+ };
1403
+ }
1404
+ return page;
1405
+ });
1406
+
1407
  fb = new FlipBook(viewer, {
1408
+ pages: validPages,
1409
  viewMode: 'webgl',
1410
  autoSize: true,
1411
  flipDuration: 800,
 
1476
  }
1477
 
1478
  /* โ”€โ”€ ๋„ค๋น„๊ฒŒ์ด์…˜ โ”€โ”€ */
1479
+ $id('homeButton').onclick=()=>{
1480
  if(fb) {
1481
  fb.destroy();
1482
  viewer.innerHTML = '';
1483
  fb = null;
1484
  }
1485
  toggle(true);
1486
+
1487
+ // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ •๋ฆฌ
1488
+ if (pageLoadingInterval) {
1489
+ clearInterval(pageLoadingInterval);
1490
+ pageLoadingInterval = null;
1491
+ }
1492
+ $id('loadingPages').style.display = 'none';
1493
+ currentLoadingPdfPath = null;
1494
  };
1495
 
1496
  function toggle(showHome){
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) {
1503
  document.body.classList.add('viewer-mode');
 
 
 
 
 
 
 
 
1504
  } else {
1505
  document.body.classList.remove('viewer-mode');
 
 
 
 
 
 
 
 
1506
  }
1507
  }
1508
 
 
1598
  window.addEventListener('DOMContentLoaded', () => {
1599
  loadServerPDFs();
1600
 
1601
+ // ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ (3์ดˆ๋งˆ๋‹ค)
1602
+ setInterval(checkCacheStatus, 3000);
1603
  });
1604
  </script>
1605
  </body>