ginipick commited on
Commit
a1b6da8
ยท
verified ยท
1 Parent(s): 8f6bd1a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1194 -541
app.py CHANGED
@@ -31,27 +31,11 @@ pdf_cache: Dict[str, Dict[str, Any]] = {}
31
  # ์บ์‹ฑ ๋ฝ
32
  cache_locks = {}
33
 
34
- ###############################################################################
35
- # (1) ํ—ฌํผ ํ•จ์ˆ˜: /pdf/test.pdf ์ฒ˜๋Ÿผ ๋„˜์–ด์˜ค๋Š” ๊ฒฝ๋กœ๋ฅผ ํ•ญ์ƒ BASE/pdf ๋‚ด๋ถ€๋กœ ๋ณด์ •
36
- ###############################################################################
37
- def get_valid_pdf_path(path: str) -> pathlib.Path:
38
- """
39
- /pdf/test.pdf, pdf/test.pdf, test.pdf ๋“ฑ์˜ ์ธ์ž๋กœ ๋“ค์–ด์™€๋„
40
- ๋ฌด์กฐ๊ฑด BASE/pdf/test.pdf ํ˜•ํƒœ์˜ ๊ฒฝ๋กœ๋ฅผ ๋Œ๋ ค์ฃผ์–ด ํŒŒ์ผ์„ ์ฐพ๋„๋ก ํ•จ.
41
- """
42
- # ์„ ํ–‰/ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ ์ œ๊ฑฐ
43
- sanitized = path.strip().lstrip("/")
44
- # ์ตœ์ข… ํŒŒ์ผ๋ช…๋งŒ ์‚ฌ์šฉ (๋””๋ ‰ํ† ๋ฆฌ ์—ญ์Šฌ๋ž˜์‹œ ๋“ฑ ๋ฐฉ์–ด)
45
- filename = os.path.basename(sanitized)
46
- return PDF_DIR / filename
47
-
48
-
49
  # PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
50
  def get_pdf_files():
51
  pdf_files = []
52
  if PDF_DIR.exists():
53
- # pdf ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด๋ถ€์˜ *.pdf ๋ชฉ๋ก
54
- pdf_files = list(PDF_DIR.glob("*.pdf"))
55
  return pdf_files
56
 
57
  # PDF ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
@@ -61,40 +45,43 @@ def generate_pdf_projects():
61
 
62
  for pdf_file in pdf_files:
63
  projects_data.append({
64
- "path": str(pdf_file.relative_to(BASE)), # ํ”„๋ก ํŠธ์—”๋“œ์—๋Š” ์ƒ๋Œ€๊ฒฝ๋กœ๋กœ ๋„˜๊ฒจ์คŒ
65
  "name": pdf_file.stem,
66
  "cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
67
  })
 
68
  return projects_data
69
 
70
- # ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ
71
  def get_cache_path(pdf_name: str):
72
  return CACHE_DIR / f"{pdf_name}_cache.json"
73
 
74
- # PDF ํŽ˜์ด์ง€ ์บ์‹ฑ ํ•จ์ˆ˜
75
  async def cache_pdf(pdf_path: str):
76
  try:
77
  import fitz # PyMuPDF
78
-
79
- # pdf_path -> ์‹ค์ œ ํŒŒ์ผ ๊ฒฝ๋กœ
80
- pdf_file = get_valid_pdf_path(pdf_path)
81
  pdf_name = pdf_file.stem
82
 
83
- # ๋™์ผ PDF ๋™์‹œ ์บ์‹ฑ ๋ฐฉ์ง€
84
  if pdf_name not in cache_locks:
85
  cache_locks[pdf_name] = threading.Lock()
86
 
87
- # ์ด๋ฏธ ์ฒ˜๋ฆฌ์ค‘/์™„๋ฃŒ๋œ ๊ฒฝ์šฐ
88
  if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
89
  logger.info(f"PDF {pdf_name} ์ด๋ฏธ ์บ์‹ฑ ์™„๋ฃŒ ๋˜๋Š” ์ง„ํ–‰ ์ค‘")
90
  return
91
 
92
  with cache_locks[pdf_name]:
 
93
  if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
94
  return
95
 
 
96
  pdf_cache[pdf_name] = {"status": "processing", "progress": 0, "pages": []}
97
 
 
98
  cache_path = get_cache_path(pdf_name)
99
  if cache_path.exists():
100
  try:
@@ -108,33 +95,44 @@ async def cache_pdf(pdf_path: str):
108
  except Exception as e:
109
  logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์‹คํŒจ: {e}")
110
 
111
- # PDF ์—ด๊ธฐ
112
- doc = fitz.open(pdf_file)
113
  total_pages = doc.page_count
114
 
115
- # ์ฒซ ํŽ˜์ด์ง€ ์ธ๋„ค์ผ
116
  if total_pages > 0:
 
117
  page = doc[0]
118
- pix_thumb = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2))
119
  thumb_data = pix_thumb.tobytes("png")
120
  b64_thumb = base64.b64encode(thumb_data).decode('utf-8')
121
  thumb_src = f"data:image/png;base64,{b64_thumb}"
122
 
 
123
  pdf_cache[pdf_name]["pages"] = [{"thumb": thumb_src, "src": ""}]
124
  pdf_cache[pdf_name]["progress"] = 1
125
  pdf_cache[pdf_name]["total_pages"] = total_pages
126
 
127
- scale_factor = 1.0
128
- jpeg_quality = 80
 
129
 
 
130
  def process_page(page_num):
131
  try:
132
  page = doc[page_num]
 
 
133
  pix = page.get_pixmap(matrix=fitz.Matrix(scale_factor, scale_factor))
 
 
134
  img_data = pix.tobytes("jpeg", jpeg_quality)
135
  b64_img = base64.b64encode(img_data).decode('utf-8')
136
  img_src = f"data:image/jpeg;base64,{b64_img}"
 
 
137
  thumb_src = "" if page_num > 0 else pdf_cache[pdf_name]["pages"][0]["thumb"]
 
138
  return {
139
  "page_num": page_num,
140
  "src": img_src,
@@ -142,38 +140,54 @@ async def cache_pdf(pdf_path: str):
142
  }
143
  except Exception as e:
144
  logger.error(f"ํŽ˜์ด์ง€ {page_num} ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
145
- return {"page_num": page_num, "src": "", "thumb": "", "error": str(e)}
 
 
 
 
 
146
 
 
147
  pages = [None] * total_pages
148
  processed_count = 0
149
- batch_size = 5
 
 
150
 
151
  for batch_start in range(0, total_pages, batch_size):
152
  batch_end = min(batch_start + batch_size, total_pages)
153
  current_batch = list(range(batch_start, batch_end))
154
 
 
155
  with concurrent.futures.ThreadPoolExecutor(max_workers=min(5, batch_size)) as executor:
156
  batch_results = list(executor.map(process_page, current_batch))
157
 
 
158
  for result in batch_results:
159
  page_num = result["page_num"]
160
- pages[page_num] = {"src": result["src"], "thumb": result["thumb"]}
 
 
 
 
161
  processed_count += 1
162
  progress = round(processed_count / total_pages * 100)
163
  pdf_cache[pdf_name]["progress"] = progress
164
 
 
165
  pdf_cache[pdf_name]["pages"] = pages
166
  try:
167
  with open(cache_path, "w") as cache_file:
168
  json.dump({
169
- "status": "processing",
170
- "progress": pdf_cache[pdf_name]["progress"],
171
  "pages": pdf_cache[pdf_name]["pages"],
172
  "total_pages": total_pages
173
  }, cache_file)
174
  except Exception as e:
175
  logger.error(f"์ค‘๊ฐ„ ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
176
 
 
177
  pdf_cache[pdf_name] = {
178
  "status": "completed",
179
  "progress": 100,
@@ -181,16 +195,18 @@ async def cache_pdf(pdf_path: str):
181
  "total_pages": total_pages
182
  }
183
 
 
184
  try:
185
  with open(cache_path, "w") as cache_file:
186
  json.dump(pdf_cache[pdf_name], cache_file)
187
  logger.info(f"PDF {pdf_name} ์บ์‹ฑ ์™„๋ฃŒ, {total_pages}ํŽ˜์ด์ง€")
188
  except Exception as e:
189
  logger.error(f"์ตœ์ข… ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
 
190
  except Exception as e:
191
  import traceback
192
  logger.error(f"PDF ์บ์‹ฑ ์˜ค๋ฅ˜: {str(e)}\n{traceback.format_exc()}")
193
- if 'pdf_name' in locals() and pdf_name in pdf_cache:
194
  pdf_cache[pdf_name]["status"] = "error"
195
  pdf_cache[pdf_name]["error"] = str(e)
196
 
@@ -199,7 +215,7 @@ async def init_cache_all_pdfs():
199
  logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
200
  pdf_files = get_pdf_files()
201
 
202
- # ๊ธฐ์กด ์บ์‹œ ๋กœ๋“œ
203
  for cache_file in CACHE_DIR.glob("*_cache.json"):
204
  try:
205
  pdf_name = cache_file.stem.replace("_cache", "")
@@ -212,126 +228,140 @@ async def init_cache_all_pdfs():
212
  except Exception as e:
213
  logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}")
214
 
215
- await asyncio.gather(*[
216
- asyncio.create_task(cache_pdf(str(pdf_file.relative_to(BASE))))
217
- for pdf_file in pdf_files
218
- if pdf_file.stem not in pdf_cache or pdf_cache[pdf_file.stem].get("status") != "completed"
219
- ])
220
 
 
221
  @app.on_event("startup")
222
  async def startup_event():
 
223
  asyncio.create_task(init_cache_all_pdfs())
224
 
225
- ###############################################################################
226
- # (2) API ๋ผ์šฐํŠธ: path -> ํ•ญ์ƒ get_valid_pdf_path()๋กœ ์‹ค์ œ ํŒŒ์ผ ์ฐพ๊ธฐ
227
- ###############################################################################
228
  @app.get("/api/pdf-projects")
229
  async def get_pdf_projects_api():
230
  return generate_pdf_projects()
231
 
 
232
  @app.get("/api/pdf-thumbnail")
233
  async def get_pdf_thumbnail(path: str):
234
  try:
235
- pdf_file = get_valid_pdf_path(path)
236
  pdf_name = pdf_file.stem
237
 
238
- # ์บ์‹œ์— ์ธ๋„ค์ผ์ด ์žˆ์œผ๋ฉด
239
  if pdf_name in pdf_cache and pdf_cache[pdf_name].get("pages"):
240
  if pdf_cache[pdf_name]["pages"][0].get("thumb"):
241
  return {"thumbnail": pdf_cache[pdf_name]["pages"][0]["thumb"]}
242
-
243
- # ์—†์œผ๋ฉด ์ง์ ‘ ์ƒ์„ฑ
244
  import fitz
245
- if not pdf_file.exists():
246
- return {"thumbnail": None, "error": "ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"}
247
-
248
- doc = fitz.open(pdf_file)
249
  if doc.page_count > 0:
250
  page = doc[0]
251
- pix = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2))
252
- img_data = pix.tobytes("jpeg", 70)
253
  b64_img = base64.b64encode(img_data).decode('utf-8')
254
 
 
255
  asyncio.create_task(cache_pdf(path))
 
256
  return {"thumbnail": f"data:image/jpeg;base64,{b64_img}"}
 
257
  return {"thumbnail": None}
258
  except Exception as e:
259
  logger.error(f"์ธ๋„ค์ผ ์ƒ์„ฑ ์˜ค๋ฅ˜: {str(e)}")
260
  return {"error": str(e), "thumbnail": None}
261
 
 
262
  @app.get("/api/cache-status")
263
  async def get_cache_status(path: str = None):
264
  if path:
265
- pdf_file = get_valid_pdf_path(path)
266
  pdf_name = pdf_file.stem
267
  if pdf_name in pdf_cache:
268
  return pdf_cache[pdf_name]
269
  return {"status": "not_cached"}
270
  else:
271
- return {
272
- name: {"status": info["status"], "progress": info.get("progress", 0)}
273
- for name, info in pdf_cache.items()
274
- }
275
 
 
276
  @app.get("/api/cached-pdf")
277
  async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
278
  try:
279
- pdf_file = get_valid_pdf_path(path)
280
  pdf_name = pdf_file.stem
281
 
 
282
  if pdf_name in pdf_cache:
283
  status = pdf_cache[pdf_name].get("status", "")
284
 
 
285
  if status == "completed":
286
  return pdf_cache[pdf_name]
 
 
287
  elif status == "processing":
288
  progress = pdf_cache[pdf_name].get("progress", 0)
289
  pages = pdf_cache[pdf_name].get("pages", [])
290
  total_pages = pdf_cache[pdf_name].get("total_pages", 0)
 
 
291
  return {
292
- "status": "processing",
293
  "progress": progress,
294
  "pages": pages,
295
  "total_pages": total_pages,
296
  "available_pages": len([p for p in pages if p and p.get("src")])
297
  }
298
 
299
- # ์บ์‹œ๊ฐ€ ์—†์œผ๋ฉด ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ
300
  background_tasks.add_task(cache_pdf, path)
301
  return {"status": "started", "progress": 0}
 
302
  except Exception as e:
303
  logger.error(f"์บ์‹œ๋œ PDF ์ œ๊ณต ์˜ค๋ฅ˜: {str(e)}")
304
  return {"error": str(e), "status": "error"}
305
 
 
306
  @app.get("/api/pdf-content")
307
  async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
308
  try:
309
- pdf_file = get_valid_pdf_path(path)
310
- pdf_name = pdf_file.stem
311
-
312
  if not pdf_file.exists():
313
- return JSONResponse(content={"error": f"ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {pdf_file}"}, status_code=404)
314
 
315
- # ์ด๋ฏธ ์บ์‹œ๋˜์—ˆ์œผ๋ฉด ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
316
- if pdf_name in pdf_cache and (
317
- pdf_cache[pdf_name].get("status") == "completed"
318
- or (pdf_cache[pdf_name].get("status") == "processing" and pdf_cache[pdf_name].get("progress", 0) > 10)
319
- ):
320
- return JSONResponse(content={"redirect": f"/api/cached-pdf?path={path}"})
321
 
322
- with open(pdf_file, "rb") as f:
323
- content = f.read()
 
 
 
324
 
 
 
 
 
 
325
  import urllib.parse
326
  filename = pdf_file.name
327
  encoded_filename = urllib.parse.quote(filename)
328
 
 
329
  background_tasks.add_task(cache_pdf, path)
330
 
 
331
  headers = {
332
  "Content-Type": "application/pdf",
333
  "Content-Disposition": f"inline; filename=\"{encoded_filename}\"; filename*=UTF-8''{encoded_filename}"
334
  }
 
335
  return Response(content=content, media_type="application/pdf", headers=headers)
336
  except Exception as e:
337
  import traceback
@@ -339,19 +369,16 @@ async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
339
  logger.error(f"PDF ์ฝ˜ํ…์ธ  ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
340
  return JSONResponse(content={"error": str(e)}, status_code=500)
341
 
342
-
343
- # HTML ํ…œํ”Œ๋ฆฟ ๊ด€๋ จ
344
  def get_html_content():
345
  html_path = BASE / "flipbook_template.html"
346
  if html_path.exists():
347
  with open(html_path, "r", encoding="utf-8") as f:
348
  return f.read()
349
- return HTML
350
 
351
- ###############################################################################
352
- # (3) HTML: pdfjsLib ๋กœ๋“œ ์—๋Ÿฌ ๋Œ€๋น„ (try/catch), .library-header์˜ pointer-events ์ œ๊ฑฐ
353
- ###############################################################################
354
- HTML = r"""
355
  <!doctype html>
356
  <html lang="ko">
357
  <head>
@@ -359,743 +386,1369 @@ HTML = r"""
359
  <title>FlipBook Space</title>
360
  <link rel="stylesheet" href="/static/flipbook.css">
361
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
362
-
363
- <!-- pdf.js ๋ฐ ๊ธฐํƒ€ ์Šคํฌ๋ฆฝํŠธ -->
364
  <script src="/static/three.js"></script>
365
  <script src="/static/iscroll.js"></script>
366
  <script src="/static/mark.js"></script>
367
  <script src="/static/mod3d.js"></script>
368
  <script src="/static/pdf.js"></script>
369
- <script>
370
- // pdfjsLib ๋กœ๋“œ ์‹คํŒจ ์‹œ ์ „์ฒด ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ค‘๋‹จ๋˜์ง€ ์•Š๋„๋ก ๋ฐฉ์–ด
371
- try {
372
- pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/pdf.worker.js';
373
- } catch(e) {
374
- console.warn("โš ๏ธ pdfjsLib not loaded: " + e);
375
- }
376
- </script>
377
-
378
  <script src="/static/flipbook.js"></script>
379
  <script src="/static/flipbook.book3.js"></script>
380
  <script src="/static/flipbook.scroll.js"></script>
381
  <script src="/static/flipbook.swipe.js"></script>
382
  <script src="/static/flipbook.webgl.js"></script>
383
-
384
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  body {
386
  margin: 0;
387
- background-image: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%);
388
- background-attachment: fixed;
389
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
390
- color: #495057;
 
 
391
  }
 
 
392
  .floating-home {
393
- position: fixed; top: 20px; left: 20px;
394
- width: 60px; height: 60px; border-radius: 50%;
 
 
 
 
395
  background: rgba(255, 255, 255, 0.9);
396
  backdrop-filter: blur(10px);
397
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
398
  z-index: 9999;
399
- display: flex; justify-content: center; align-items: center;
400
- cursor: pointer; transition: 0.3s; overflow: hidden;
 
 
 
 
 
 
 
 
 
401
  }
402
- .floating-home:hover { transform: scale(1.05); }
403
  .floating-home .icon {
404
- width: 100%; height: 100%;
405
- font-size: 22px; color: #a5d8ff; transition: 0.3s;
406
- display: flex; justify-content: center; align-items: center;
 
 
 
 
 
407
  }
408
- .floating-home:hover .icon { color: #8bc5f8; }
 
 
 
 
409
  .floating-home .title {
410
- position: absolute; left: 70px;
 
411
  background: rgba(255, 255, 255, 0.95);
412
- padding: 8px 20px; border-radius: 20px;
413
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
414
- font-weight: 600; font-size: 14px;
415
- white-space: nowrap; pointer-events: none; opacity: 0;
416
- transform: translateX(-10px); transition: all 0.3s ease;
 
 
 
 
 
417
  }
 
418
  .floating-home:hover .title {
419
- opacity: 1; transform: translateX(0);
420
- }
421
- /* โ–ผ pointer-events: none ์ œ๊ฑฐ! ๋ฒ„ํŠผ ํด๋ฆญ์„ ๋ง‰์ง€ ์•Š๋Š”๋‹ค. */
422
- .library-header {
423
- position: fixed; top: 10px; left:0; right:0;
424
- text-align: center; z-index: 100;
425
- }
426
- .library-header .title {
427
- display: inline-block;
428
- padding: 8px 20px;
429
- background: rgba(255, 255, 255, 0.85);
430
- backdrop-filter: blur(10px);
431
- border-radius: 30px;
432
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
433
- font-size: 1.2rem; font-weight: 600;
434
- background-image: linear-gradient(120deg, #667eea 0%, #764ba2 100%);
435
- -webkit-background-clip: text; background-clip: text; color: transparent;
436
  }
 
437
  #home, #viewerPage {
438
- padding-top: 100px; max-width: 1200px; margin: 0 auto;
439
- padding-bottom: 60px; padding-left: 30px; padding-right: 30px; position: relative;
 
 
 
 
 
440
  }
 
 
441
  .upload-container {
442
- display: flex; margin-bottom: 30px; justify-content: center;
 
 
443
  }
 
444
  button.upload {
445
- all: unset; cursor: pointer; padding: 12px 20px; border-radius: 12px;
446
- background: white; margin: 0 10px; font-weight: 500;
447
- display: inline-flex; align-items: center; position: relative;
448
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
449
- transition: 0.3s; overflow: hidden;
 
 
 
 
 
 
 
 
450
  }
 
451
  button.upload::before {
452
- content: ''; position: absolute; top:0; left:0; width:100%; height:100%;
453
- background: linear-gradient(120deg, #a5d8ff, #ffd6e0);
454
- opacity: 0.08; z-index: -1;
 
 
 
 
 
 
455
  }
 
456
  button.upload:hover {
457
  transform: translateY(-3px);
458
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
459
  }
460
- button.upload:hover::before { opacity: 0.15; }
461
- button.upload i { margin-right: 8px; font-size: 20px; }
462
 
463
- .section-title {
464
- font-size: 1.3rem; font-weight:600; margin:30px 0 15px; color:#495057;
465
  }
466
- .grid {
467
- display: grid; grid-template-columns: repeat(auto-fill, minmax(200px,1fr));
468
- gap:24px; margin-top:36px;
 
469
  }
470
- .no-projects {
471
- text-align:center; margin:40px 0; font-size:16px; color:#495057;
 
 
 
 
 
472
  }
 
473
  .card {
474
- background: #ffffff; border-radius:12px; cursor: pointer;
475
- box-shadow: 0 2px 8px rgba(0,0,0,0.05); width:100%; height:280px;
476
- position:relative; display:flex; flex-direction:column;
477
- align-items:center; justify-content:center; transition: 0.3s; overflow:hidden;
 
 
 
 
 
 
 
 
 
478
  }
 
479
  .card::before {
480
- content:''; position:absolute; top:0; left:0; width:100%; height:100%;
481
- background: linear-gradient(135deg, #ffd6e0 0%, #a5d8ff 100%);
482
- opacity:0.06; z-index:1;
 
 
 
 
 
 
483
  }
 
484
  .card::after {
485
- content:''; position:absolute; top:0; left:0; width:100%; height:30%;
 
 
 
 
 
486
  background: linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
487
- z-index:2;
488
  }
 
489
  .card img {
490
- width:65%; height:auto; object-fit: contain; position:absolute;
491
- top:50%; left:50%; transform: translate(-50%, -65%);
492
- border:1px solid rgba(0,0,0,0.05);
493
- box-shadow: 0 4px 15px rgba(0,0,0,0.08); z-index:3; transition:0.3s;
 
 
 
 
 
 
 
494
  }
 
495
  .card:hover {
496
  transform: translateY(-5px);
497
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
498
  }
 
499
  .card:hover img {
500
  transform: translate(-50%, -65%) scale(1.03);
501
  box-shadow: 0 8px 20px rgba(0,0,0,0.12);
502
  }
 
503
  .card p {
504
- position: absolute; bottom:20px; left:50%; transform:translateX(-50%);
505
- background: rgba(255,255,255,0.9); padding:8px 16px;
506
- border-radius:30px; box-shadow:0 2px 10px rgba(0,0,0,0.05);
507
- width:80%; text-align:center; white-space:nowrap; overflow:hidden;
508
- text-overflow:ellipsis; font-size:14px; font-weight:500; color:#495057; z-index:4;
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  }
 
510
  .card:hover p {
511
- background: rgba(255,255,255,0.95);
512
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
513
  }
 
 
514
  .cached-status {
515
- position:absolute; top:10px; right:10px;
516
- background:#d0bfff; color:white; font-size:11px; padding:3px 8px;
517
- border-radius:12px; z-index:5; box-shadow:0 2px 8px rgba(0,0,0,0.05);
 
 
 
 
 
 
 
518
  }
 
 
519
  #viewer {
520
- width:90%; height:90vh; max-width:90%; margin:0;
521
- background:#fff; border:none; border-radius:16px; position:fixed;
522
- top:50%; left:50%; transform:translate(-50%,-50%);
523
- z-index:1000; box-shadow:0 8px 24px rgba(0,0,0,0.12);
524
- max-height:calc(90vh - 40px); aspect-ratio:auto; object-fit:contain; overflow:hidden;
 
 
 
 
 
 
 
 
 
 
 
 
525
  }
 
 
526
  .flipbook-container .fb3d-menu-bar {
527
- z-index:2000 !important; opacity:1 !important; bottom:0 !important;
528
- background-color:rgba(255,255,255,0.9) !important; backdrop-filter:blur(10px) !important;
529
- border-radius:0 0 16px 16px !important; padding:12px 0 !important;
530
- box-shadow:0 -4px 20px rgba(0,0,0,0.1) !important;
531
- }
532
- .flipbook-container .fb3d-menu-bar > ul > li {
533
- margin:0 12px !important;
 
534
  }
 
535
  .flipbook-container .fb3d-menu-bar > ul > li > img,
536
  .flipbook-container .fb3d-menu-bar > ul > li > div {
537
- opacity:1 !important; transform:scale(1.2) !important;
538
- filter:drop-shadow(0 2px 3px rgba(0,0,0,0.1)) !important;
 
539
  }
 
 
 
 
 
 
540
  .flipbook-container .fb3d-menu-bar > ul > li > span {
541
- background-color:rgba(0,0,0,0.7) !important; color:white !important;
542
- border-radius:8px !important; padding:8px 12px !important; font-size:13px !important;
543
- bottom:55px !important; font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
544
- letter-spacing:0.3px !important;
 
 
 
 
545
  }
 
 
546
  .viewer-mode {
547
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
548
  }
 
 
549
  #viewerPage {
550
- background:transparent;
551
  }
552
- @keyframes spin { 0%{transform:rotate(0deg);}100%{transform:rotate(360deg);} }
 
 
 
 
 
 
553
  .loading-spinner {
554
- border:4px solid rgba(255,255,255,0.3);
555
- border-top:4px solid #a5d8ff; border-radius:50%;
556
- width:50px; height:50px; margin:0 auto; animation:spin 1.5s ease-in-out infinite;
 
 
 
 
557
  }
 
558
  .loading-container {
559
- position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
560
- text-align:center; background:rgba(255,255,255,0.85);
561
- backdrop-filter:blur(10px); padding:30px; border-radius:12px;
562
- box-shadow:0 4px 12px rgba(0,0,0,0.08); z-index:9999;
 
 
 
 
 
 
 
563
  }
 
564
  .loading-text {
565
- margin-top:20px; font-size:16px; color:#495057; font-weight:500;
 
 
 
566
  }
567
- @keyframes fadeIn { from{opacity:0;} to{opacity:1;} }
568
- .fade-in { animation:fadeIn 0.5s ease-out; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  .progress-bar-container {
570
- width:100%; height:6px; background-color:rgba(0,0,0,0.1);
571
- border-radius:3px; margin-top:15px; overflow:hidden;
 
 
 
 
572
  }
 
573
  .progress-bar {
574
- height:100%; background:linear-gradient(to right, #a5d8ff, #d0bfff);
575
- border-radius:3px; transition:width 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  .loading-pages {
578
- position:absolute; bottom:20px; left:50%; transform:translateX(-50%);
579
- background:rgba(255,255,255,0.9); padding:10px 20px; border-radius:20px;
580
- box-shadow:0 4px 12px rgba(0,0,0,0.08); font-size:14px; color:#495057;
581
- z-index:9998; text-align:center;
 
 
 
 
 
 
 
 
582
  }
583
- @media (max-width:768px){
584
- .grid { grid-template-columns: repeat(auto-fill, minmax(160px,1fr)); gap:16px; }
585
- .card { height:240px; }
586
- .library-header .title { font-size:1rem; padding:6px 16px; }
587
- .floating-home { width:50px; height:50px; }
588
- .floating-home .icon { font-size:18px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  }
590
  </style>
591
  </head>
592
  <body>
 
593
  <div id="homeButton" class="floating-home" style="display:none;">
594
  <div class="icon"><i class="fas fa-home"></i></div>
595
  <div class="title">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</div>
596
  </div>
 
 
597
  <div class="library-header">
598
  <div class="title">FlipBook Library</div>
599
  </div>
600
 
601
  <section id="home" class="fade-in">
602
  <div class="upload-container">
603
- <button class="upload" id="imageUploadBtn">
604
- <i class="fas fa-images"></i> ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
605
- <input id="imgInput" type="file" accept="image/*" multiple hidden>
606
- </button>
607
- <button class="upload" id="pdfUploadBtn">
608
- <i class="fas fa-file-pdf"></i> PDF ์ถ”๊ฐ€
609
- <input id="pdfInput" type="file" accept="application/pdf" hidden>
 
 
610
  </button>
611
  </div>
612
 
613
  <div class="section-title">๋‚ด ํ”„๋กœ์ ํŠธ</div>
614
- <div class="grid" id="grid"></div>
615
- <div id="noProjects" class="no-projects" style="display:none;">
 
 
616
  ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๋‚˜ PDF๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
617
  </div>
618
  </section>
619
 
620
- <section id="viewerPage" style="display:none;">
621
  <div id="viewer"></div>
622
- <div id="loadingPages" class="loading-pages" style="display:none;">
623
- ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0/0</span>
624
- </div>
625
  </section>
626
 
627
  <script>
628
- let projects = [], fb = null;
 
 
 
 
629
  let serverProjects = [];
 
 
630
  let currentLoadingPdfPath = null;
631
  let pageLoadingInterval = null;
632
 
633
- function $id(id){return document.getElementById(id)}
634
-
635
  ['click','touchstart'].forEach(evt=>{
636
- document.addEventListener(evt,function u(){
637
- new Audio('static/turnPage2.mp3').play().then(a=>a.pause()).catch(()=>{});
638
- document.removeEventListener(evt,u,{capture:true});
639
- }, {once:true,capture:true});
640
  });
641
-
642
- function setupDirectEvents() {
643
- const imageBtn = $id('imageUploadBtn');
644
- const imageInput = $id('imgInput');
645
- if (imageBtn && imageInput) {
646
- imageBtn.onclick = (e)=>{
647
- e.preventDefault(); e.stopPropagation();
648
- imageInput.click();
649
- };
650
- }
651
- const pdfBtn = $id('pdfUploadBtn');
652
- const pdfInput = $id('pdfInput');
653
- if (pdfBtn && pdfInput) {
654
- pdfBtn.onclick = (e)=>{
655
- e.preventDefault(); e.stopPropagation();
656
- pdfInput.click();
657
- };
658
- }
659
  }
660
 
661
- function addCard(i, thumb, title, isCached=false) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  const d = document.createElement('div');
663
  d.className = 'card fade-in';
664
- d.onclick = ()=> open(i);
665
- const displayTitle = title.length>15 ? title.substring(0,15)+'...' : title;
666
- const cachedBadge = isCached ? `<div class="cached-status">์บ์‹œ๋จ</div>` : '';
 
 
 
 
 
 
 
 
667
  d.innerHTML = `
668
  <div class="card-inner">
669
  ${cachedBadge}
670
  <img src="${thumb}" alt="${displayTitle}" loading="lazy">
671
- <p title="${title}">${displayTitle}</p>
672
  </div>
673
  `;
674
- $id('grid').appendChild(d);
 
 
675
  $id('noProjects').style.display = 'none';
676
  }
677
 
678
- $id('imgInput').onchange = e => {
679
- const files = [...e.target.files]; if(!files.length) return;
 
 
 
680
  showLoading("์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘...");
681
- const pages=[], tot=files.length; let done=0;
682
- files.forEach((f,i)=>{
683
- const r=new FileReader();
684
- r.onload=x=>{
685
- pages[i] = {src:x.target.result, thumb:x.target.result};
686
- if(++done===tot){ save(pages,'์ด๋ฏธ์ง€ ์ปฌ๋ ‰์…˜'); hideLoading(); }
687
- };
688
- r.readAsDataURL(f);
689
- });
690
  };
691
 
692
- $id('pdfInput').onchange = e => {
 
693
  const file=e.target.files[0]; if(!file) return;
 
 
694
  showLoading("PDF ๋กœ๋”ฉ ์ค‘...");
 
695
  const fr=new FileReader();
696
  fr.onload=v=>{
697
- if(!window.pdfjsLib) {
698
- hideLoading();
699
- showError("pdf.js๊ฐ€ ๋กœ๋“œ๋˜์ง€ ์•Š์•„ PDF ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
700
- return;
701
- }
702
  pdfjsLib.getDocument({data:v.target.result}).promise.then(async pdf=>{
703
  const pages=[];
 
704
  for(let p=1;p<=pdf.numPages;p++){
705
- updateLoading(\`PDF ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... (\${p}/\${pdf.numPages})\`);
 
 
706
  const pg=await pdf.getPage(p), vp=pg.getViewport({scale:1});
707
- const c=document.createElement('canvas'); c.width=vp.width; c.height=vp.height;
708
  await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
709
- pages.push({src:c.toDataURL(), thumb:c.toDataURL()});
710
  }
 
711
  hideLoading();
712
- save(pages, file.name.replace('.pdf',''));
713
- }).catch(err=>{
714
- console.error("PDF ๋กœ๋”ฉ ์˜ค๋ฅ˜:", err);
715
  hideLoading();
716
  showError("PDF ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
717
  });
718
- };
719
- fr.readAsArrayBuffer(file);
720
  };
721
 
722
- function save(pages,title,isCached=false){
 
723
  const id=projects.push(pages)-1;
724
  addCard(id, pages[0].thumb, title, isCached);
725
  }
726
 
 
727
  async function loadServerPDFs() {
728
  try {
729
- if(document.querySelectorAll('.card').length===0){
 
730
  showLoading("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘...");
731
  }
732
- // ์บ์‹œ ์ƒํƒœ
733
- const csRes = await fetch('/api/cache-status');
734
- const cacheStatus = await csRes.json();
735
-
736
- // PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก
737
- const resp = await fetch('/api/pdf-projects');
738
- serverProjects = await resp.json();
739
- if(serverProjects.length===0){
 
 
740
  hideLoading();
741
- $id('noProjects').style.display='block';
742
  return;
743
  }
744
- const thumbPromises = serverProjects.map(async (proj,idx)=>{
745
- updateLoading(\`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (\${idx+1}/\${serverProjects.length})\`);
746
- const pdfName = proj.name;
 
 
 
747
  const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
 
748
  try {
749
- const resp2 = await fetch(\`/api/pdf-thumbnail?path=\${encodeURIComponent(proj.path)}\`);
750
- const data = await resp2.json();
751
- if(data.thumbnail){
 
 
 
 
 
 
 
 
 
752
  return {
753
- pages:[{
754
- src:data.thumbnail,
755
- thumb:data.thumbnail,
756
- path:proj.path,
757
- cached:isCached
758
- }],
759
- name:proj.name,
760
  isCached
761
  };
762
  }
763
- } catch(e) {
764
- console.error(\`์ธ๋„ค์ผ ๋กœ๋“œ ์˜ค๋ฅ˜ (\${proj.name}):\`, e);
765
  }
 
766
  return null;
767
  });
768
- const results = await Promise.all(thumbPromises);
769
- results.filter(r=>r).forEach(r=>{
770
- save(r.pages, r.name, r.isCached);
 
 
 
 
771
  });
 
772
  hideLoading();
773
- if(document.querySelectorAll('.card').length===0){
774
- $id('noProjects').style.display='block';
 
 
775
  }
776
- } catch(e) {
777
- console.error("์„œ๋ฒ„ PDF ๋กœ๋“œ ์‹คํŒจ:", e);
778
  hideLoading();
779
  showError("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
780
  }
781
  }
782
 
783
- async function checkCacheStatus(){
 
784
  try {
785
- const res=await fetch('/api/cache-status');
786
- const cacheStatus=await res.json();
787
- const cards=document.querySelectorAll('.card');
788
- for(let i=0;i<cards.length;i++){
789
- if(projects[i]?.[0]?.path){
790
- const pdfPath=projects[i][0].path;
791
- const pdfName = pdfPath.split('/').pop().replace('.pdf','');
 
 
 
 
 
792
  let badgeEl = cards[i].querySelector('.cached-status');
793
- if(cacheStatus[pdfName]?.status==="completed"){
794
- if(!badgeEl){
795
- badgeEl=document.createElement('div');
796
- badgeEl.className='cached-status';
797
- badgeEl.textContent='์บ์‹œ๋จ';
 
798
  cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
799
- } else if(badgeEl.textContent!=='์บ์‹œ๋จ'){
800
- badgeEl.textContent='์บ์‹œ๋จ';
801
- badgeEl.style.background='#d0bfff';
802
  }
803
- projects[i][0].cached=true;
804
- } else if(cacheStatus[pdfName]?.status==="processing"){
805
- if(!badgeEl){
806
- badgeEl=document.createElement('div');
807
- badgeEl.className='cached-status';
808
  cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
809
  }
810
- badgeEl.textContent=\`\${cacheStatus[pdfName].progress}%\`;
811
- badgeEl.style.background='#ffd6e0';
812
  }
813
  }
814
  }
815
- if(currentLoadingPdfPath && pageLoadingInterval){
816
- const pdfName = currentLoadingPdfPath.split('/').pop().replace('.pdf','');
817
- if(cacheStatus[pdfName]){
818
- const st=cacheStatus[pdfName].status;
819
- const pr=cacheStatus[pdfName].progress||0;
820
- if(st==="completed"){
 
 
 
 
 
821
  clearInterval(pageLoadingInterval);
822
- $id('loadingPages').style.display='none';
823
- currentLoadingPdfPath=null;
 
 
824
  refreshFlipBook();
825
- } else if(st==="processing"){
826
- $id('loadingPages').style.display='block';
827
- $id('loadingPagesCount').textContent=\`\${pr}%\`;
 
828
  }
829
  }
830
  }
831
- } catch(e){
832
- console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
 
833
  }
834
  }
835
 
836
- async function open(i){
 
837
  toggle(false);
838
- const pages=projects[i];
839
- if(fb){fb.destroy();viewer.innerHTML='';}
840
- if(pages[0]?.path){
 
 
 
 
 
 
 
841
  const pdfPath = pages[0].path;
842
- let progressive=false;
843
- currentLoadingPdfPath=pdfPath;
844
- if(pages[0].cached){
 
 
 
 
 
845
  showLoading("์บ์‹œ๋œ PDF ๋กœ๋”ฉ ์ค‘...");
 
846
  try {
847
- const resp=await fetch(\`/api/cached-pdf?path=\${encodeURIComponent(pdfPath)}\`);
848
- const cData=await resp.json();
849
- if(cData.status==="completed" && cData.pages){
850
- hideLoading(); createFlipBook(cData.pages); currentLoadingPdfPath=null; return;
851
- } else if(cData.status==="processing" && cData.pages && cData.pages.length>0){
852
- hideLoading(); createFlipBook(cData.pages);
853
- progressive=true;
854
- startProgressiveLoadingIndicator(cData.progress,cData.total_pages);
 
 
 
 
 
 
 
 
855
  }
856
- }catch(e){console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", e);}
 
 
 
857
  }
858
- if(!progressive){
 
 
859
  showLoading("PDF ์ค€๋น„ ์ค‘...");
 
860
  try {
861
- const resp=await fetch(\`/api/pdf-content?path=\${encodeURIComponent(pdfPath)}\`);
862
- const d=await resp.json();
863
- if(d.redirect){
864
- const r2=await fetch(d.redirect);
865
- const c2=await r2.json();
866
- if(c2.status==="completed" && c2.pages){
867
- hideLoading(); createFlipBook(c2.pages); currentLoadingPdfPath=null; return;
868
- } else if(c2.status==="processing" && c2.pages && c2.pages.length>0){
869
- hideLoading(); createFlipBook(c2.pages);
870
- startProgressiveLoadingIndicator(c2.progress,c2.total_pages);
 
 
 
 
 
 
 
 
 
 
871
  return;
872
  }
873
  }
874
- // ์‹ค์ œ PDF ๋ฐ์ดํ„ฐ (arrayBuffer)
875
- const pdfResp = await fetch(\`/api/pdf-content?path=\${encodeURIComponent(pdfPath)}\`);
876
- // ํ˜น์‹œ JSON ํ˜•ํƒœ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๊ฐ€ ์˜ค๋Š”์ง€ ์ฒดํฌ
877
- try{
878
- const testJson=await pdfResp.clone().json();
879
- if(testJson.redirect){
880
- const rr=await fetch(testJson.redirect);
881
- const cc=await rr.json();
882
- if(cc.pages && cc.pages.length>0){
883
- hideLoading(); createFlipBook(cc.pages);
884
- if(cc.status==="processing") {
885
- startProgressiveLoadingIndicator(cc.progress,cc.total_pages);
 
 
 
 
 
886
  } else {
887
- currentLoadingPdfPath=null;
888
  }
889
  return;
890
  }
891
  }
892
- }catch(e){}
893
- // ArrayBuffer โ†’ pdf.js ๋กœ๋“œ
894
- const pdfData=await pdfResp.arrayBuffer();
895
- if(!window.pdfjsLib){
896
- hideLoading();
897
- showError("pdf.js๊ฐ€ ๋กœ๋“œ๋˜์ง€ ์•Š์•„ PDF ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
898
- return;
899
  }
900
- const pdf=await pdfjsLib.getDocument({data:pdfData}).promise;
901
- const pdfPages=[];
902
- for(let p=1;p<=pdf.numPages;p++){
903
- updateLoading(\`ํŽ˜์ด์ง€ ์ค€๋น„ ์ค‘... (\${p}/\${pdf.numPages})\`);
904
- const pg=await pdf.getPage(p);
905
- const vp=pg.getViewport({scale:1});
906
- const c=document.createElement('canvas');
907
- c.width=vp.width; c.height=vp.height;
908
- await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
909
- pdfPages.push({src:c.toDataURL(), thumb:c.toDataURL()});
 
 
 
 
 
 
 
 
 
910
  }
 
911
  hideLoading();
912
  createFlipBook(pdfPages);
913
- currentLoadingPdfPath=null;
914
- } catch(err){
915
- console.error("PDF ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:", err);
 
916
  hideLoading();
917
- showError("PDF ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
918
- currentLoadingPdfPath=null;
919
  }
920
  }
921
  } else {
 
922
  createFlipBook(pages);
923
- currentLoadingPdfPath=null;
924
  }
925
  }
926
 
927
- function startProgressiveLoadingIndicator(progress, totalPages){
928
- $id('loadingPages').style.display='block';
929
- $id('loadingPagesCount').textContent=\`\${progress}%\`;
930
- if(pageLoadingInterval){ clearInterval(pageLoadingInterval); }
931
- pageLoadingInterval = setInterval(async()=>{
932
- if(!currentLoadingPdfPath){
 
 
 
 
 
 
 
 
933
  clearInterval(pageLoadingInterval);
934
- $id('loadingPages').style.display='none';
935
  return;
936
  }
 
937
  try {
938
- const r=await fetch(\`/api/cache-status?path=\${encodeURIComponent(currentLoadingPdfPath)}\`);
939
- const st=await r.json();
940
- if(st.status==="completed"){
 
 
941
  clearInterval(pageLoadingInterval);
942
- $id('loadingPages').style.display='none';
943
- refreshFlipBook();
944
- currentLoadingPdfPath=null;
945
- } else if(st.status==="processing"){
946
- $id('loadingPagesCount').textContent=\`\${st.progress}%\`;
947
  }
948
- }catch(e){console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);}
 
 
949
  }, 1000);
950
  }
951
 
952
- async function refreshFlipBook(){
953
- if(!currentLoadingPdfPath || !fb)return;
954
- try{
955
- const r=await fetch(\`/api/cached-pdf?path=\${encodeURIComponent(currentLoadingPdfPath)}\`);
956
- const cData=await r.json();
957
- if(cData.status==="completed" && cData.pages){
958
- fb.destroy(); viewer.innerHTML='';
959
- createFlipBook(cData.pages);
960
- currentLoadingPdfPath=null;
 
 
 
 
 
 
 
961
  }
962
- } catch(e){console.error("ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜:", e);}
 
 
963
  }
964
 
965
- function createFlipBook(pages){
966
- try{
967
- const calcSize=()=>{
968
- const ww=window.innerWidth, wh=window.innerHeight;
969
- const ar=ww/wh; let w,h;
970
- if(ar>1){
971
- h=Math.min(wh*0.9,wh-40);
972
- w=h*ar*0.8;
973
- if(w>ww*0.9){w=ww*0.9; h=w/(ar*0.8);}
974
- } else {
975
- w=Math.min(ww*0.9,ww-40);
976
- h=w/ar*0.9;
977
- if(h>wh*0.9){h=wh*0.9; w=h*ar*0.9;}
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  }
979
- return {width:Math.round(w), height:Math.round(h)};
 
 
 
 
 
980
  };
981
- const size=calcSize();
982
- viewer.style.width=size.width+'px';
983
- viewer.style.height=size.height+'px';
984
-
985
- const validPages = pages.map(p=>{
986
- if(!p || !p.src){
987
- return {src:'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+', thumb:''};
 
 
 
 
 
 
 
988
  }
989
- return p;
990
  });
991
- fb=new FlipBook(viewer,{
 
992
  pages: validPages,
993
- viewMode:'webgl', autoSize:true, flipDuration:800, backgroundColor:'#fff',
994
- sound:true,
995
- assets:{flipMp3:'static/turnPage2.mp3',hardFlipMp3:'static/turnPage2.mp3'},
996
- controlsProps:{
997
- enableFullscreen:true, enableToc:true, enableDownload:false,
998
- enablePrint:false, enableZoom:true, enableShare:false,
999
- enableSearch:true, enableAutoPlay:true, enableAnnotation:false,
1000
- enableSound:true, enableLightbox:false,
1001
- layout:10, skin:'light', autoNavigationTime:3600, hideControls:false,
1002
- paddingTop:10, paddingLeft:10, paddingRight:10, paddingBottom:10,
1003
- pageTextureSize:1024, thumbnails:true, autoHideControls:false, controlsTimeout:8000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1004
  }
1005
  });
1006
- window.addEventListener('resize',()=>{
1007
- if(fb){
1008
- const s=calcSize();
1009
- viewer.style.width=s.width+'px'; viewer.style.height=s.height+'px';
 
 
 
1010
  fb.resize();
1011
  }
1012
  });
1013
- setTimeout(()=>{
1014
- try{
1015
- const bars=document.querySelectorAll('.flipbook-container .fb3d-menu-bar');
1016
- bars.forEach(b=>{b.style.display='block'; b.style.opacity='1'; b.style.visibility='visible'; b.style.zIndex='9999';});
1017
- }catch(e){console.warn("์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜:", e);}
1018
- },1000);
1019
- } catch(e){
1020
- console.error("FlipBook ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜:", e);
1021
- showError("FlipBook ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1022
  }
1023
  }
1024
 
 
1025
  $id('homeButton').onclick=()=>{
1026
- if(fb){fb.destroy();viewer.innerHTML='';fb=null;}
 
 
 
 
1027
  toggle(true);
1028
- if(pageLoadingInterval){clearInterval(pageLoadingInterval); pageLoadingInterval=null;}
1029
- $id('loadingPages').style.display='none';
1030
- currentLoadingPdfPath=null;
 
 
 
 
 
1031
  };
1032
 
1033
  function toggle(showHome){
1034
- $id('home').style.display= showHome?'block':'none';
1035
- $id('viewerPage').style.display= showHome?'none':'block';
1036
- $id('homeButton').style.display= showHome?'none':'block';
1037
- if(!showHome){ document.body.classList.add('viewer-mode'); }
1038
- else { document.body.classList.remove('viewer-mode'); }
 
 
 
 
 
1039
  }
1040
 
1041
- function showLoading(msg, progress=-1){
 
 
1042
  hideLoading();
1043
- const c=document.createElement('div'); c.className='loading-container fade-in'; c.id='loadingContainer';
1044
- let pb='';
1045
- if(progress>=0){
1046
- pb=`
 
 
 
 
1047
  <div class="progress-bar-container">
1048
- <div id="progressBar" class="progress-bar" style="width:${progress}%;"></div>
1049
  </div>
1050
  `;
1051
  }
1052
- c.innerHTML=`
 
1053
  <div class="loading-spinner"></div>
1054
- <p class="loading-text" id="loadingText">${msg||'๋กœ๋”ฉ ์ค‘...'}</p>
1055
- ${pb}
1056
  `;
1057
- document.body.appendChild(c);
 
1058
  }
1059
- function updateLoading(msg, progress=-1){
1060
- const t=$id('loadingText');
1061
- if(t) t.textContent=msg;
1062
- if(progress>=0){
1063
- let bar=$id('progressBar');
1064
- if(!bar){
1065
- const lc=$id('loadingContainer');
1066
- if(lc){
1067
- const pc=document.createElement('div');
1068
- pc.className='progress-bar-container';
1069
- pc.innerHTML=`<div id="progressBar" class="progress-bar" style="width:${progress}%;"></div>`;
1070
- lc.appendChild(pc);
1071
- bar=$id('progressBar');
 
 
 
 
 
1072
  }
1073
  } else {
1074
- bar.style.width=`${progress}%`;
1075
  }
1076
  }
1077
  }
1078
- function hideLoading(){
1079
- const lc=$id('loadingContainer');
1080
- if(lc) lc.remove();
 
 
 
1081
  }
1082
- function showError(msg){
1083
- const ex=$id('errorContainer'); if(ex) ex.remove();
1084
- const c=document.createElement('div'); c.className='loading-container fade-in'; c.id='errorContainer';
1085
- c.innerHTML=`
1086
- <p class="loading-text" style="color:#e74c3c;">${msg}</p>
1087
- <button id="errorCloseBtn" style="margin-top:15px; padding:8px 16px; background:#3498db; color:white; border:none; border-radius:4px; cursor:pointer;">ํ™•์ธ</button>
 
 
 
 
 
 
 
 
1088
  `;
1089
- document.body.appendChild(c);
1090
- $id('errorCloseBtn').onclick=()=> c.remove();
1091
- setTimeout(()=>{ if($id('errorContainer')) $id('errorContainer').remove(); },5000);
 
 
 
 
 
 
 
 
 
 
 
1092
  }
1093
 
1094
- window.addEventListener('DOMContentLoaded', ()=>{
 
 
 
 
 
1095
  setupDirectEvents();
 
 
 
 
 
 
 
1096
  loadServerPDFs();
1097
- setInterval(checkCacheStatus,3000);
 
 
1098
  });
 
 
1099
  </script>
1100
  </body>
1101
  </html>
@@ -1106,4 +1759,4 @@ async def root():
1106
  return get_html_content()
1107
 
1108
  if __name__ == "__main__":
1109
- uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
 
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 ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
 
45
 
46
  for pdf_file in 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
54
 
55
+ # ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ ์ƒ์„ฑ
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
63
+
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:
 
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,
 
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,
 
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
208
  logger.error(f"PDF ์บ์‹ฑ ์˜ค๋ฅ˜: {str(e)}\n{traceback.format_exc()}")
209
+ if pdf_name in pdf_cache:
210
  pdf_cache[pdf_name]["status"] = "error"
211
  pdf_cache[pdf_name]["error"] = str(e)
212
 
 
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", "")
 
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")
239
  async def startup_event():
240
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒœ์Šคํฌ๋กœ ์บ์‹ฑ ์‹คํ–‰
241
  asyncio.create_task(init_cache_all_pdfs())
242
 
243
+ # API ์—”๋“œํฌ์ธํŠธ: PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก
 
 
244
  @app.get("/api/pdf-projects")
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:
252
+ pdf_file = pathlib.Path(path)
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:
276
  logger.error(f"์ธ๋„ค์ผ ์ƒ์„ฑ ์˜ค๋ฅ˜: {str(e)}")
277
  return {"error": str(e), "thumbnail": None}
278
 
279
+ # API ์—”๋“œํฌ์ธํŠธ: ์บ์‹œ ์ƒํƒœ ํ™•์ธ
280
  @app.get("/api/cache-status")
281
  async def get_cache_status(path: str = None):
282
  if path:
283
+ pdf_file = pathlib.Path(path)
284
  pdf_name = pdf_file.stem
285
  if pdf_name in pdf_cache:
286
  return pdf_cache[pdf_name]
287
  return {"status": "not_cached"}
288
  else:
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:
296
+ pdf_file = pathlib.Path(path)
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)
324
  return {"status": "started", "progress": 0}
325
+
326
  except Exception as e:
327
  logger.error(f"์บ์‹œ๋œ PDF ์ œ๊ณต ์˜ค๋ฅ˜: {str(e)}")
328
  return {"error": str(e), "status": "error"}
329
 
330
+ # API ์—”๋“œํฌ์ธํŠธ: PDF ์›๋ณธ ์ฝ˜ํ…์ธ  ์ œ๊ณต(์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)
331
  @app.get("/api/pdf-content")
332
  async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
333
  try:
334
+ # ์บ์‹ฑ ์ƒํƒœ ํ™•์ธ
335
+ pdf_file = pathlib.Path(path)
 
336
  if not pdf_file.exists():
337
+ return JSONResponse(content={"error": f"ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {path}"}, status_code=404)
338
 
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
+ # ํŒŒ์ผ ์ฝ๊ธฐ
348
+ with open(path, "rb") as pdf_file:
349
+ content = pdf_file.read()
350
+
351
+ # ํŒŒ์ผ๋ช… ์ฒ˜๋ฆฌ
352
  import urllib.parse
353
  filename = pdf_file.name
354
  encoded_filename = urllib.parse.quote(filename)
355
 
356
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
357
  background_tasks.add_task(cache_pdf, path)
358
 
359
+ # ์‘๋‹ต ํ—ค๋” ์„ค์ •
360
  headers = {
361
  "Content-Type": "application/pdf",
362
  "Content-Disposition": f"inline; filename=\"{encoded_filename}\"; filename*=UTF-8''{encoded_filename}"
363
  }
364
+
365
  return Response(content=content, media_type="application/pdf", headers=headers)
366
  except Exception as e:
367
  import traceback
 
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"
375
  if html_path.exists():
376
  with open(html_path, "r", encoding="utf-8") as f:
377
  return f.read()
378
+ return HTML # ๊ธฐ๋ณธ HTML ์‚ฌ์šฉ
379
 
380
+ # HTML ๋ฌธ์ž์—ด (UI ์ˆ˜์ • ๋ฒ„์ „)
381
+ HTML = """
 
 
382
  <!doctype html>
383
  <html lang="ko">
384
  <head>
 
386
  <title>FlipBook Space</title>
387
  <link rel="stylesheet" href="/static/flipbook.css">
388
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
 
389
  <script src="/static/three.js"></script>
390
  <script src="/static/iscroll.js"></script>
391
  <script src="/static/mark.js"></script>
392
  <script src="/static/mod3d.js"></script>
393
  <script src="/static/pdf.js"></script>
 
 
 
 
 
 
 
 
 
394
  <script src="/static/flipbook.js"></script>
395
  <script src="/static/flipbook.book3.js"></script>
396
  <script src="/static/flipbook.scroll.js"></script>
397
  <script src="/static/flipbook.swipe.js"></script>
398
  <script src="/static/flipbook.webgl.js"></script>
 
399
  <style>
400
+ /* ์ „์ฒด ์‚ฌ์ดํŠธ ํŒŒ์Šคํ…”ํ†ค ํ…Œ๋งˆ */
401
+ :root {
402
+ --primary-color: #a5d8ff; /* ํŒŒ์Šคํ…” ๋ธ”๋ฃจ */
403
+ --secondary-color: #ffd6e0; /* ํŒŒ์Šคํ…” ํ•‘ํฌ */
404
+ --tertiary-color: #c3fae8; /* ํŒŒ์Šคํ…” ๋ฏผํŠธ */
405
+ --accent-color: #d0bfff; /* ํŒŒ์Šคํ…” ํผํ”Œ */
406
+ --bg-color: #f8f9fa; /* ๋ฐ์€ ๋ฐฐ๊ฒฝ */
407
+ --text-color: #495057; /* ๋ถ€๋“œ๋Ÿฌ์šด ์–ด๋‘์šด ์ƒ‰ */
408
+ --card-bg: #ffffff; /* ์นด๋“œ ๋ฐฐ๊ฒฝ์ƒ‰ */
409
+ --shadow-sm: 0 2px 8px rgba(0,0,0,0.05);
410
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
411
+ --shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
412
+ --radius-sm: 8px;
413
+ --radius-md: 12px;
414
+ --radius-lg: 16px;
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 {
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
+ display: flex;
454
+ justify-content: center;
455
+ align-items: center;
456
+ width: 100%;
457
+ height: 100%;
458
+ font-size: 22px;
459
+ color: var(--primary-color);
460
+ transition: var(--transition);
461
  }
462
+
463
+ .floating-home:hover .icon {
464
+ color: #8bc5f8;
465
+ }
466
+
467
  .floating-home .title {
468
+ position: absolute;
469
+ left: 70px;
470
  background: rgba(255, 255, 255, 0.95);
471
+ padding: 8px 20px;
472
+ border-radius: 20px;
473
+ box-shadow: var(--shadow-sm);
474
+ font-weight: 600;
475
+ font-size: 14px;
476
+ white-space: nowrap;
477
+ pointer-events: none;
478
+ opacity: 0;
479
+ transform: translateX(-10px);
480
+ transition: all 0.3s ease;
481
  }
482
+
483
  .floating-home:hover .title {
484
+ opacity: 1;
485
+ transform: translateX(0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  }
487
+
488
  #home, #viewerPage {
489
+ padding-top: 100px;
490
+ max-width: 1200px;
491
+ margin: 0 auto;
492
+ padding-bottom: 60px;
493
+ padding-left: 30px;
494
+ padding-right: 30px;
495
+ position: relative;
496
  }
497
+
498
+ /* ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
499
  .upload-container {
500
+ display: flex;
501
+ margin-bottom: 30px;
502
+ justify-content: center;
503
  }
504
+
505
  button.upload {
506
+ all: unset;
507
+ cursor: pointer;
508
+ padding: 12px 20px;
509
+ border-radius: var(--radius-md);
510
+ background: white;
511
+ margin: 0 10px;
512
+ font-weight: 500;
513
+ display: flex;
514
+ align-items: center;
515
+ box-shadow: var(--shadow-sm);
516
+ transition: var(--transition);
517
+ position: relative;
518
+ overflow: hidden;
519
  }
520
+
521
  button.upload::before {
522
+ content: '';
523
+ position: absolute;
524
+ top: 0;
525
+ left: 0;
526
+ width: 100%;
527
+ height: 100%;
528
+ background: linear-gradient(120deg, var(--primary-color) 0%, var(--secondary-color) 100%);
529
+ opacity: 0.08;
530
+ z-index: -1;
531
  }
532
+
533
  button.upload:hover {
534
  transform: translateY(-3px);
535
+ box-shadow: var(--shadow-md);
536
  }
 
 
537
 
538
+ button.upload:hover::before {
539
+ opacity: 0.15;
540
  }
541
+
542
+ button.upload i {
543
+ margin-right: 8px;
544
+ font-size: 20px;
545
  }
546
+
547
+ /* ๊ทธ๋ฆฌ๋“œ ๋ฐ ์นด๋“œ ์Šคํƒ€์ผ */
548
+ .grid {
549
+ display: grid;
550
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
551
+ gap: 24px;
552
+ margin-top: 36px;
553
  }
554
+
555
  .card {
556
+ background: var(--card-bg);
557
+ border-radius: var(--radius-md);
558
+ cursor: pointer;
559
+ box-shadow: var(--shadow-sm);
560
+ width: 100%;
561
+ height: 280px;
562
+ position: relative;
563
+ display: flex;
564
+ flex-direction: column;
565
+ align-items: center;
566
+ justify-content: center;
567
+ transition: var(--transition);
568
+ overflow: hidden;
569
  }
570
+
571
  .card::before {
572
+ content: '';
573
+ position: absolute;
574
+ top: 0;
575
+ left: 0;
576
+ width: 100%;
577
+ height: 100%;
578
+ background: linear-gradient(135deg, var(--secondary-color) 0%, var(--primary-color) 100%);
579
+ opacity: 0.06;
580
+ z-index: 1;
581
  }
582
+
583
  .card::after {
584
+ content: '';
585
+ position: absolute;
586
+ top: 0;
587
+ left: 0;
588
+ width: 100%;
589
+ height: 30%;
590
  background: linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
591
+ z-index: 2;
592
  }
593
+
594
  .card img {
595
+ width: 65%;
596
+ height: auto;
597
+ object-fit: contain;
598
+ position: absolute;
599
+ top: 50%;
600
+ left: 50%;
601
+ transform: translate(-50%, -65%);
602
+ border: 1px solid rgba(0,0,0,0.05);
603
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
604
+ z-index: 3;
605
+ transition: var(--transition);
606
  }
607
+
608
  .card:hover {
609
  transform: translateY(-5px);
610
+ box-shadow: var(--shadow-md);
611
  }
612
+
613
  .card:hover img {
614
  transform: translate(-50%, -65%) scale(1.03);
615
  box-shadow: 0 8px 20px rgba(0,0,0,0.12);
616
  }
617
+
618
  .card p {
619
+ position: absolute;
620
+ bottom: 20px;
621
+ left: 50%;
622
+ transform: translateX(-50%);
623
+ background: rgba(255, 255, 255, 0.9);
624
+ padding: 8px 16px;
625
+ border-radius: 30px;
626
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
627
+ width: 80%;
628
+ text-align: center;
629
+ white-space: nowrap;
630
+ overflow: hidden;
631
+ text-overflow: ellipsis;
632
+ font-size: 14px;
633
+ font-weight: 500;
634
+ color: var(--text-color);
635
+ z-index: 4;
636
+ transition: var(--transition);
637
  }
638
+
639
  .card:hover p {
640
+ background: rgba(255, 255, 255, 0.95);
641
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
642
  }
643
+
644
+ /* ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ */
645
  .cached-status {
646
+ position: absolute;
647
+ top: 10px;
648
+ right: 10px;
649
+ background: var(--accent-color);
650
+ color: white;
651
+ font-size: 11px;
652
+ padding: 3px 8px;
653
+ border-radius: 12px;
654
+ z-index: 5;
655
+ box-shadow: var(--shadow-sm);
656
  }
657
+
658
+ /* ๋ทฐ์–ด ์Šคํƒ€์ผ */
659
  #viewer {
660
+ width: 90%;
661
+ height: 90vh;
662
+ max-width: 90%;
663
+ margin: 0;
664
+ background: var(--card-bg);
665
+ border: none;
666
+ border-radius: var(--radius-lg);
667
+ position: fixed;
668
+ top: 50%;
669
+ left: 50%;
670
+ transform: translate(-50%, -50%);
671
+ z-index: 1000;
672
+ box-shadow: var(--shadow-lg);
673
+ max-height: calc(90vh - 40px);
674
+ aspect-ratio: auto;
675
+ object-fit: contain;
676
+ overflow: hidden;
677
  }
678
+
679
+ /* FlipBook ์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ */
680
  .flipbook-container .fb3d-menu-bar {
681
+ z-index: 2000 !important;
682
+ opacity: 1 !important;
683
+ bottom: 0 !important;
684
+ background-color: rgba(255,255,255,0.9) !important;
685
+ backdrop-filter: blur(10px) !important;
686
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg) !important;
687
+ padding: 12px 0 !important;
688
+ box-shadow: 0 -4px 20px rgba(0,0,0,0.1) !important;
689
  }
690
+
691
  .flipbook-container .fb3d-menu-bar > ul > li > img,
692
  .flipbook-container .fb3d-menu-bar > ul > li > div {
693
+ opacity: 1 !important;
694
+ transform: scale(1.2) !important;
695
+ filter: drop-shadow(0 2px 3px rgba(0,0,0,0.1)) !important;
696
  }
697
+
698
+ .flipbook-container .fb3d-menu-bar > ul > li {
699
+ margin: 0 12px !important;
700
+ }
701
+
702
+ /* ๋ฉ”๋‰ด ํˆดํŒ ์Šคํƒ€์ผ */
703
  .flipbook-container .fb3d-menu-bar > ul > li > span {
704
+ background-color: rgba(0,0,0,0.7) !important;
705
+ color: white !important;
706
+ border-radius: var(--radius-sm) !important;
707
+ padding: 8px 12px !important;
708
+ font-size: 13px !important;
709
+ bottom: 55px !important;
710
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
711
+ letter-spacing: 0.3px !important;
712
  }
713
+
714
+ /* ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ๋ฐฐ๊ฒฝ ์˜ค๋ฒ„๋ ˆ์ด */
715
  .viewer-mode {
716
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
717
  }
718
+
719
+ /* ๋ทฐ์–ด ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ */
720
  #viewerPage {
721
+ background: transparent;
722
  }
723
+
724
+ /* ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
725
+ @keyframes spin {
726
+ 0% { transform: rotate(0deg); }
727
+ 100% { transform: rotate(360deg); }
728
+ }
729
+
730
  .loading-spinner {
731
+ border: 4px solid rgba(255,255,255,0.3);
732
+ border-top: 4px solid var(--primary-color);
733
+ border-radius: 50%;
734
+ width: 50px;
735
+ height: 50px;
736
+ margin: 0 auto;
737
+ animation: spin 1.5s ease-in-out infinite;
738
  }
739
+
740
  .loading-container {
741
+ position: absolute;
742
+ top: 50%;
743
+ left: 50%;
744
+ transform: translate(-50%, -50%);
745
+ text-align: center;
746
+ background: rgba(255, 255, 255, 0.85);
747
+ backdrop-filter: blur(10px);
748
+ padding: 30px;
749
+ border-radius: var(--radius-md);
750
+ box-shadow: var(--shadow-md);
751
+ z-index: 9999;
752
  }
753
+
754
  .loading-text {
755
+ margin-top: 20px;
756
+ font-size: 16px;
757
+ color: var(--text-color);
758
+ font-weight: 500;
759
  }
760
+
761
+ /* ํŽ˜์ด์ง€ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
762
+ @keyframes fadeIn {
763
+ from { opacity: 0; }
764
+ to { opacity: 1; }
765
+ }
766
+
767
+ .fade-in {
768
+ animation: fadeIn 0.5s ease-out;
769
+ }
770
+
771
+ /* ์ถ”๊ฐ€ ์Šคํƒ€์ผ */
772
+ .section-title {
773
+ font-size: 1.3rem;
774
+ font-weight: 600;
775
+ margin: 30px 0 15px;
776
+ color: var(--text-color);
777
+ }
778
+
779
+ .no-projects {
780
+ text-align: center;
781
+ margin: 40px 0;
782
+ color: var(--text-color);
783
+ font-size: 16px;
784
+ }
785
+
786
+ /* ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” */
787
  .progress-bar-container {
788
+ width: 100%;
789
+ height: 6px;
790
+ background-color: rgba(0,0,0,0.1);
791
+ border-radius: 3px;
792
+ margin-top: 15px;
793
+ overflow: hidden;
794
  }
795
+
796
  .progress-bar {
797
+ height: 100%;
798
+ background: linear-gradient(to right, var(--primary-color), var(--accent-color));
799
+ border-radius: 3px;
800
+ transition: width 0.3s ease;
801
+ }
802
+
803
+ /* ํ—ค๋” ๋กœ๊ณ  ๋ฐ ํƒ€์ดํ‹€ */
804
+ .library-header {
805
+ position: fixed;
806
+ top: 20px;
807
+ left: 0;
808
+ right: 0;
809
+ text-align: center;
810
+ z-index: 100;
811
+ pointer-events: none;
812
  }
813
+
814
+ .library-header .title {
815
+ display: inline-block;
816
+ padding: 12px 30px;
817
+ background: rgba(255, 255, 255, 0.85);
818
+ backdrop-filter: blur(10px);
819
+ border-radius: 30px;
820
+ box-shadow: var(--shadow-md);
821
+ font-size: 1.5rem;
822
+ font-weight: 600;
823
+ background-image: linear-gradient(120deg, #667eea 0%, #764ba2 100%);
824
+ -webkit-background-clip: text;
825
+ background-clip: text;
826
+ color: transparent;
827
+ pointer-events: all;
828
+ }
829
+
830
+ /* ์ ์ง„์  ๋กœ๋”ฉ ํ‘œ์‹œ */
831
  .loading-pages {
832
+ position: absolute;
833
+ bottom: 20px;
834
+ left: 50%;
835
+ transform: translateX(-50%);
836
+ background: rgba(255, 255, 255, 0.9);
837
+ padding: 10px 20px;
838
+ border-radius: 20px;
839
+ box-shadow: var(--shadow-md);
840
+ font-size: 14px;
841
+ color: var(--text-color);
842
+ z-index: 9998;
843
+ text-align: center;
844
  }
845
+
846
+ /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
847
+ @media (max-width: 768px) {
848
+ .grid {
849
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
850
+ gap: 16px;
851
+ }
852
+
853
+ .card {
854
+ height: 240px;
855
+ }
856
+
857
+ .library-header .title {
858
+ font-size: 1.25rem;
859
+ padding: 10px 20px;
860
+ }
861
+
862
+ .floating-home {
863
+ width: 50px;
864
+ height: 50px;
865
+ }
866
+
867
+ .floating-home .icon {
868
+ font-size: 18px;
869
+ }
870
  }
871
  </style>
872
  </head>
873
  <body>
874
+ <!-- ์ œ๋ชฉ์„ Home ๋ฒ„ํŠผ๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์–ด๋กœ ์ฒ˜๋ฆฌ -->
875
  <div id="homeButton" class="floating-home" style="display:none;">
876
  <div class="icon"><i class="fas fa-home"></i></div>
877
  <div class="title">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</div>
878
  </div>
879
+
880
+ <!-- ์„ผํ„ฐ ์ •๋ ฌ๋œ ํƒ€์ดํ‹€ -->
881
  <div class="library-header">
882
  <div class="title">FlipBook Library</div>
883
  </div>
884
 
885
  <section id="home" class="fade-in">
886
  <div class="upload-container">
887
+
888
+ <button class="upload" id="imageUploadBtn" onclick="document.getElementById('imgInput').click(); return false;">
889
+ <i class="fas fa-images"></i> ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
890
+ <input id="imgInput" type="file" accept="image/*" multiple hidden>
891
+ </button>
892
+ <button class="upload" id="pdfUploadBtn" onclick="document.getElementById('pdfInput').click(); return false;">
893
+ <i class="fas fa-file-pdf"></i> PDF ์ถ”๊ฐ€
894
+ <input id="pdfInput" type="file" accept="application/pdf" hidden>
895
+
896
  </button>
897
  </div>
898
 
899
  <div class="section-title">๋‚ด ํ”„๋กœ์ ํŠธ</div>
900
+ <div class="grid" id="grid">
901
+ <!-- ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
902
+ </div>
903
+ <div id="noProjects" class="no-projects" style="display: none;">
904
  ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๋‚˜ PDF๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
905
  </div>
906
  </section>
907
 
908
+ <section id="viewerPage" style="display:none">
909
  <div id="viewer"></div>
910
+ <div id="loadingPages" class="loading-pages" style="display:none;">ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0/0</span></div>
 
 
911
  </section>
912
 
913
  <script>
914
+ let projects=[], fb=null;
915
+ const grid=$id('grid'), viewer=$id('viewer');
916
+ pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
917
+
918
+ // ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
919
  let serverProjects = [];
920
+
921
+ // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ
922
  let currentLoadingPdfPath = null;
923
  let pageLoadingInterval = null;
924
 
925
+ /* ๐Ÿ”Š ์˜ค๋””์˜ค unlock โ€“ ๋‚ด์žฅ Audio ์™€ ๊ฐ™์€ MP3 ๊ฒฝ๋กœ ์‚ฌ์šฉ */
 
926
  ['click','touchstart'].forEach(evt=>{
927
+ document.addEventListener(evt,function u(){new Audio('static/turnPage2.mp3')
928
+ .play().then(a=>a.pause()).catch(()=>{});document.removeEventListener(evt,u,{capture:true});},
929
+ {once:true,capture:true});
 
930
  });
931
+
932
+ // ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ ํ•จ์ˆ˜
933
+ function setupUploadButtons() {
934
+ // ๋ชจ๋“  ์—…๋กœ๋“œ ๋ฒ„ํŠผ์— ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
935
+ document.querySelectorAll('.upload').forEach(button => {
936
+ button.addEventListener('click', function(e) {
937
+ // ๋ฒ„ํŠผ ๋‚ด๋ถ€์˜ input ์š”์†Œ ์ฐพ๊ธฐ
938
+ const inputElement = this.querySelector('input[type="file"]');
939
+ if (inputElement) {
940
+ // input ์š”์†Œ ํด๋ฆญ (ํŒŒ์ผ ์„ ํƒ ๋‹ค์ด์–ผ๋กœ๊ทธ ์—ด๊ธฐ)
941
+ inputElement.click();
942
+ e.preventDefault(); // ๋ฒ„ํŠผ ๊ธฐ๋ณธ ๋™์ž‘ ๋ฐฉ์ง€
943
+ }
944
+ });
945
+ });
 
 
 
946
  }
947
 
948
+ /* โ”€โ”€ ์œ ํ‹ธ โ”€โ”€ */
949
+ function $id(id){return document.getElementById(id)}
950
+
951
+ // ์ง์ ‘ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ • (๋” ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•)
952
+ // ์ˆ˜์ •๋œ ์ด๋ฒคํŠธ ์„ค์ • ํ•จ์ˆ˜
953
+ function setupDirectEvents() {
954
+ console.log('์ด๋ฒคํŠธ ์„ค์ • ์‹œ์ž‘');
955
+
956
+ // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ
957
+ const imageBtn = document.getElementById('imageUploadBtn');
958
+ const imageInput = document.getElementById('imgInput');
959
+
960
+ if (imageBtn && imageInput) {
961
+ console.log('์ด๋ฏธ์ง€ ๋ฒ„ํŠผ ์ฐพ์Œ:', imageBtn);
962
+ // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ฑฐํ•˜๊ณ  ๋‹ค์‹œ ์ถ”๊ฐ€
963
+ imageBtn.removeEventListener('click', imageClickHandler);
964
+ imageBtn.addEventListener('click', imageClickHandler);
965
+
966
+ function imageClickHandler(e) {
967
+ console.log('์ด๋ฏธ์ง€ ๋ฒ„ํŠผ ํด๋ฆญ๋จ');
968
+ e.preventDefault();
969
+ e.stopPropagation();
970
+ imageInput.click();
971
+ }
972
+ } else {
973
+ console.error('์ด๋ฏธ์ง€ ๋ฒ„ํŠผ ๋˜๋Š” ์ธํ’‹์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ');
974
+ }
975
+
976
+ // PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ
977
+ const pdfBtn = document.getElementById('pdfUploadBtn');
978
+ const pdfInput = document.getElementById('pdfInput');
979
+
980
+ if (pdfBtn && pdfInput) {
981
+ console.log('PDF ๋ฒ„ํŠผ ์ฐพ์Œ:', pdfBtn);
982
+ // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ฑฐํ•˜๊ณ  ๋‹ค์‹œ ์ถ”๊ฐ€
983
+ pdfBtn.removeEventListener('click', pdfClickHandler);
984
+ pdfBtn.addEventListener('click', pdfClickHandler);
985
+
986
+ function pdfClickHandler(e) {
987
+ console.log('PDF ๋ฒ„ํŠผ ํด๋ฆญ๋จ');
988
+ e.preventDefault();
989
+ e.stopPropagation();
990
+ pdfInput.click();
991
+ }
992
+ } else {
993
+ console.error('PDF ๋ฒ„ํŠผ ๋˜๋Š” ์ธํ’‹์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ');
994
+ }
995
+
996
+ // ํŒŒ์ผ ์„ ํƒ ์ด๋ฒคํŠธ ์žฌ์„ค์ •
997
+ if (imageInput) {
998
+ imageInput.onchange = function(e) {
999
+ console.log('์ด๋ฏธ์ง€ ํŒŒ์ผ ์„ ํƒ๋จ:', e.target.files.length);
1000
+ const files = [...e.target.files];
1001
+ if (!files.length) return;
1002
+
1003
+ showLoading("์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘...");
1004
+
1005
+ const pages = [], tot = files.length;
1006
+ let done = 0;
1007
+
1008
+ files.forEach((f, i) => {
1009
+ const r = new FileReader();
1010
+ r.onload = x => {
1011
+ pages[i] = {src: x.target.result, thumb: x.target.result};
1012
+ if (++done === tot) {
1013
+ save(pages, '์ด๋ฏธ์ง€ ์ปฌ๋ ‰์…˜');
1014
+ hideLoading();
1015
+ }
1016
+ };
1017
+ r.readAsDataURL(f);
1018
+ });
1019
+ };
1020
+ }
1021
+
1022
+ if (pdfInput) {
1023
+ pdfInput.onchange = function(e) {
1024
+ console.log('PDF ํŒŒ์ผ ์„ ํƒ๋จ:', e.target.files.length);
1025
+ const file = e.target.files[0];
1026
+ if (!file) return;
1027
+
1028
+ showLoading("PDF ๋กœ๋”ฉ ์ค‘...");
1029
+
1030
+ const fr = new FileReader();
1031
+ fr.onload = v => {
1032
+ pdfjsLib.getDocument({data: v.target.result}).promise.then(async pdf => {
1033
+ const pages = [];
1034
+
1035
+ for (let p = 1; p <= pdf.numPages; p++) {
1036
+ updateLoading(`PDF ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... (${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
+ await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
1044
+ pages.push({src: c.toDataURL(), thumb: c.toDataURL()});
1045
+ }
1046
+
1047
+ hideLoading();
1048
+ save(pages, file.name.replace('.pdf', ''));
1049
+ }).catch(error => {
1050
+ console.error("PDF ๋กœ๋”ฉ ์˜ค๋ฅ˜:", error);
1051
+ hideLoading();
1052
+ showError("PDF ๋กœ๏ฟฝ๏ฟฝ๏ฟฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1053
+ });
1054
+ };
1055
+ fr.readAsArrayBuffer(file);
1056
+ };
1057
+ }
1058
+
1059
+ console.log('์ด๋ฒคํŠธ ์„ค์ • ์™„๋ฃŒ');
1060
+ }
1061
+
1062
+ function addCard(i, thumb, title, isCached = false) {
1063
  const d = document.createElement('div');
1064
  d.className = 'card fade-in';
1065
+ d.onclick = () => open(i);
1066
+
1067
+ // ์ œ๋ชฉ ์ฒ˜๋ฆฌ
1068
+ const displayTitle = title ?
1069
+ (title.length > 15 ? title.substring(0, 15) + '...' : title) :
1070
+ 'ํ”„๋กœ์ ํŠธ ' + (i+1);
1071
+
1072
+ // ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์ถ”๊ฐ€
1073
+ const cachedBadge = isCached ?
1074
+ '<div class="cached-status">์บ์‹œ๋จ</div>' : '';
1075
+
1076
  d.innerHTML = `
1077
  <div class="card-inner">
1078
  ${cachedBadge}
1079
  <img src="${thumb}" alt="${displayTitle}" loading="lazy">
1080
+ <p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
1081
  </div>
1082
  `;
1083
+ grid.appendChild(d);
1084
+
1085
+ // ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์œผ๋ฉด 'ํ”„๋กœ์ ํŠธ ์—†์Œ' ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
1086
  $id('noProjects').style.display = 'none';
1087
  }
1088
 
1089
+ /* โ”€โ”€ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ โ”€โ”€ */
1090
+ $id('imgInput').onchange=e=>{
1091
+ const files=[...e.target.files]; if(!files.length) return;
1092
+
1093
+ // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
1094
  showLoading("์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘...");
1095
+
1096
+ const pages=[],tot=files.length;let done=0;
1097
+ files.forEach((f,i)=>{const r=new FileReader();r.onload=x=>{pages[i]={src:x.target.result,thumb:x.target.result};
1098
+ if(++done===tot) {
1099
+ save(pages, '์ด๋ฏธ์ง€ ์ปฌ๋ ‰์…˜');
1100
+ hideLoading();
1101
+ }
1102
+ };r.readAsDataURL(f);});
 
1103
  };
1104
 
1105
+ /* โ”€โ”€ PDF ์—…๋กœ๋“œ โ”€โ”€ */
1106
+ $id('pdfInput').onchange=e=>{
1107
  const file=e.target.files[0]; if(!file) return;
1108
+
1109
+ // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
1110
  showLoading("PDF ๋กœ๋”ฉ ์ค‘...");
1111
+
1112
  const fr=new FileReader();
1113
  fr.onload=v=>{
 
 
 
 
 
1114
  pdfjsLib.getDocument({data:v.target.result}).promise.then(async pdf=>{
1115
  const pages=[];
1116
+
1117
  for(let p=1;p<=pdf.numPages;p++){
1118
+ // ๋กœ๋”ฉ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1119
+ updateLoading(`PDF ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... (${p}/${pdf.numPages})`);
1120
+
1121
  const pg=await pdf.getPage(p), vp=pg.getViewport({scale:1});
1122
+ const c=document.createElement('canvas');c.width=vp.width;c.height=vp.height;
1123
  await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
1124
+ pages.push({src:c.toDataURL(),thumb:c.toDataURL()});
1125
  }
1126
+
1127
  hideLoading();
1128
+ save(pages, file.name.replace('.pdf', ''));
1129
+ }).catch(error => {
1130
+ console.error("PDF ๋กœ๋”ฉ ์˜ค๋ฅ˜:", error);
1131
  hideLoading();
1132
  showError("PDF ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1133
  });
1134
+ };fr.readAsArrayBuffer(file);
 
1135
  };
1136
 
1137
+ /* โ”€โ”€ ํ”„๋กœ์ ํŠธ ์ €์žฅ โ”€โ”€ */
1138
+ function save(pages, title, isCached = false){
1139
  const id=projects.push(pages)-1;
1140
  addCard(id, pages[0].thumb, title, isCached);
1141
  }
1142
 
1143
+ /* โ”€โ”€ ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ โ”€โ”€ */
1144
  async function loadServerPDFs() {
1145
  try {
1146
+ // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
1147
+ if (document.querySelectorAll('.card').length === 0) {
1148
  showLoading("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘...");
1149
  }
1150
+
1151
+ // ๋จผ์ € ์บ์‹œ ์ƒํƒœ ํ™•์ธ
1152
+ const cacheStatusRes = await fetch('/api/cache-status');
1153
+ const cacheStatus = await cacheStatusRes.json();
1154
+
1155
+ // PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
1156
+ const response = await fetch('/api/pdf-projects');
1157
+ serverProjects = await response.json();
1158
+
1159
+ if (serverProjects.length === 0) {
1160
  hideLoading();
1161
+ $id('noProjects').style.display = 'block';
1162
  return;
1163
  }
1164
+
1165
+ // ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์ธ๋„ค์ผ ์ƒ์„ฑ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์ตœ์ ํ™”)
1166
+ const thumbnailPromises = serverProjects.map(async (project, index) => {
1167
+ updateLoading(`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (${index+1}/${serverProjects.length})`);
1168
+
1169
+ const pdfName = project.name;
1170
  const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
1171
+
1172
  try {
1173
+ // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
1174
+ const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`);
1175
+ const data = await response.json();
1176
+
1177
+ if(data.thumbnail) {
1178
+ const pages = [{
1179
+ src: data.thumbnail,
1180
+ thumb: data.thumbnail,
1181
+ path: project.path,
1182
+ cached: isCached
1183
+ }];
1184
+
1185
  return {
1186
+ pages,
1187
+ name: project.name,
 
 
 
 
 
1188
  isCached
1189
  };
1190
  }
1191
+ } catch (err) {
1192
+ console.error(`์ธ๋„ค์ผ ๋กœ๋“œ ์˜ค๋ฅ˜ (${project.name}):`, err);
1193
  }
1194
+
1195
  return null;
1196
  });
1197
+
1198
+ // ๋ชจ๋“  ์ธ๋„ค์ผ ์š”์ฒญ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ
1199
+ const results = await Promise.all(thumbnailPromises);
1200
+
1201
+ // ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ ๊ฒฐ๊ณผ๋งŒ ํ‘œ์‹œ
1202
+ results.filter(result => result !== null).forEach(result => {
1203
+ save(result.pages, result.name, result.isCached);
1204
  });
1205
+
1206
  hideLoading();
1207
+
1208
+ // ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
1209
+ if (document.querySelectorAll('.card').length === 0) {
1210
+ $id('noProjects').style.display = 'block';
1211
  }
1212
+ } catch(error) {
1213
+ console.error('์„œ๋ฒ„ PDF ๋กœ๋“œ ์‹คํŒจ:', error);
1214
  hideLoading();
1215
  showError("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1216
  }
1217
  }
1218
 
1219
+ /* โ”€โ”€ ์บ์‹œ ์ƒํƒœ ์ •๊ธฐ์ ์œผ๋กœ ํ™•์ธ โ”€โ”€ */
1220
+ async function checkCacheStatus() {
1221
  try {
1222
+ const response = await fetch('/api/cache-status');
1223
+ const cacheStatus = await response.json();
1224
+
1225
+ // ํ˜„์žฌ ์นด๋“œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1226
+ const cards = document.querySelectorAll('.card');
1227
+
1228
+ for(let i = 0; i < cards.length; i++) {
1229
+ if(projects[i] && projects[i][0] && projects[i][0].path) {
1230
+ const pdfPath = projects[i][0].path;
1231
+ const pdfName = pdfPath.split('/').pop().replace('.pdf', '');
1232
+
1233
+ // ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์—…๋ฐ์ดํŠธ
1234
  let badgeEl = cards[i].querySelector('.cached-status');
1235
+
1236
+ if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed") {
1237
+ if(!badgeEl) {
1238
+ badgeEl = document.createElement('div');
1239
+ badgeEl.className = 'cached-status';
1240
+ badgeEl.textContent = '์บ์‹œ๋จ';
1241
  cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1242
+ } else if (badgeEl.textContent !== '์บ์‹œ๋จ') {
1243
+ badgeEl.textContent = '์บ์‹œ๋จ';
1244
+ badgeEl.style.background = 'var(--accent-color)';
1245
  }
1246
+ projects[i][0].cached = true;
1247
+ } else if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "processing") {
1248
+ if(!badgeEl) {
1249
+ badgeEl = document.createElement('div');
1250
+ badgeEl.className = 'cached-status';
1251
  cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1252
  }
1253
+ badgeEl.textContent = `${cacheStatus[pdfName].progress}%`;
1254
+ badgeEl.style.background = 'var(--secondary-color)';
1255
  }
1256
  }
1257
  }
1258
+
1259
+ // ํ˜„์žฌ ๋กœ๋”ฉ ์ค‘์ธ PDF๊ฐ€ ์žˆ์œผ๋ฉด ์ƒํƒœ ํ™•์ธ
1260
+ if (currentLoadingPdfPath && pageLoadingInterval) {
1261
+ const pdfName = currentLoadingPdfPath.split('/').pop().replace('.pdf', '');
1262
+
1263
+ if (cacheStatus[pdfName]) {
1264
+ const status = cacheStatus[pdfName].status;
1265
+ const progress = cacheStatus[pdfName].progress || 0;
1266
+
1267
+ if (status === "completed") {
1268
+ // ์บ์‹ฑ ์™„๋ฃŒ ์‹œ
1269
  clearInterval(pageLoadingInterval);
1270
+ $id('loadingPages').style.display = 'none';
1271
+ currentLoadingPdfPath = null;
1272
+
1273
+ // ์™„๋ฃŒ๋œ ์บ์‹œ๋กœ ํ”Œ๋ฆฝ๋ถ ๋‹ค์‹œ ๋กœ๋“œ
1274
  refreshFlipBook();
1275
+ } else if (status === "processing") {
1276
+ // ์ง„ํ–‰ ์ค‘์ผ ๋•Œ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
1277
+ $id('loadingPages').style.display = 'block';
1278
+ $id('loadingPagesCount').textContent = `${progress}%`;
1279
  }
1280
  }
1281
  }
1282
+
1283
+ } catch(error) {
1284
+ console.error('์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
1285
  }
1286
  }
1287
 
1288
+ /* โ”€โ”€ ์นด๋“œ โ†’ FlipBook โ”€โ”€ */
1289
+ async function open(i) {
1290
  toggle(false);
1291
+ const pages = projects[i];
1292
+
1293
+ // ๊ธฐ์กด FlipBook ์ •๋ฆฌ
1294
+ if(fb) {
1295
+ fb.destroy();
1296
+ viewer.innerHTML = '';
1297
+ }
1298
+
1299
+ // ์„œ๋ฒ„ PDF ๋˜๋Š” ๋กœ์ปฌ ํ”„๋กœ์ ํŠธ ์ฒ˜๋ฆฌ
1300
+ if(pages[0].path) {
1301
  const pdfPath = pages[0].path;
1302
+
1303
+ // ์ ์ง„์  ๋กœ๋”ฉ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™”
1304
+ let progressiveLoading = false;
1305
+ currentLoadingPdfPath = pdfPath;
1306
+
1307
+ // ์บ์‹œ ์—ฌ๋ถ€ ํ™•์ธ
1308
+ if(pages[0].cached) {
1309
+ // ์บ์‹œ๋œ PDF ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
1310
  showLoading("์บ์‹œ๋œ PDF ๋กœ๋”ฉ ์ค‘...");
1311
+
1312
  try {
1313
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
1314
+ const cachedData = await response.json();
1315
+
1316
+ if(cachedData.status === "completed" && cachedData.pages) {
1317
+ hideLoading();
1318
+ createFlipBook(cachedData.pages);
1319
+ currentLoadingPdfPath = null;
1320
+ return;
1321
+ } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1322
+ // ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
1323
+ hideLoading();
1324
+ createFlipBook(cachedData.pages);
1325
+ progressiveLoading = true;
1326
+
1327
+ // ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
1328
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1329
  }
1330
+ } catch(error) {
1331
+ console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
1332
+ // ์บ์‹œ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์›๋ณธ PDF๋กœ ๋Œ€์ฒด
1333
+ }
1334
  }
1335
+
1336
+ if (!progressiveLoading) {
1337
+ // ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
1338
  showLoading("PDF ์ค€๋น„ ์ค‘...");
1339
+
1340
  try {
1341
+ const response = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1342
+ const data = await response.json();
1343
+
1344
+ // ์บ์‹œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋œ ๊ฒฝ์šฐ
1345
+ if(data.redirect) {
1346
+ const redirectRes = await fetch(data.redirect);
1347
+ const cachedData = await redirectRes.json();
1348
+
1349
+ if(cachedData.status === "completed" && cachedData.pages) {
1350
+ hideLoading();
1351
+ createFlipBook(cachedData.pages);
1352
+ currentLoadingPdfPath = null;
1353
+ return;
1354
+ } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1355
+ // ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
1356
+ hideLoading();
1357
+ createFlipBook(cachedData.pages);
1358
+
1359
+ // ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
1360
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1361
  return;
1362
  }
1363
  }
1364
+
1365
+ // ์›๋ณธ PDF ๋กœ๋“œ (ArrayBuffer ํ˜•ํƒœ)
1366
+ const pdfResponse = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1367
+
1368
+ // JSON ์‘๋‹ต์ธ ๊ฒฝ์šฐ (๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋“ฑ)
1369
+ try {
1370
+ const jsonData = await pdfResponse.clone().json();
1371
+ if (jsonData.redirect) {
1372
+ const redirectRes = await fetch(jsonData.redirect);
1373
+ const cachedData = await redirectRes.json();
1374
+
1375
+ if(cachedData.pages && cachedData.pages.length > 0) {
1376
+ hideLoading();
1377
+ createFlipBook(cachedData.pages);
1378
+
1379
+ if(cachedData.status === "processing") {
1380
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1381
  } else {
1382
+ currentLoadingPdfPath = null;
1383
  }
1384
  return;
1385
  }
1386
  }
1387
+ } catch (e) {
1388
+ // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์›๋ณธ PDF ๋ฐ์ดํ„ฐ๋กœ ์ฒ˜๋ฆฌ
 
 
 
 
 
1389
  }
1390
+
1391
+ // ArrayBuffer ํ˜•ํƒœ์˜ PDF ๋ฐ์ดํ„ฐ
1392
+ const pdfData = await pdfResponse.arrayBuffer();
1393
+
1394
+ // PDF ๋กœ๋“œ ๋ฐ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
1395
+ const pdf = await pdfjsLib.getDocument({data: pdfData}).promise;
1396
+ const pdfPages = [];
1397
+
1398
+ for(let p = 1; p <= pdf.numPages; p++) {
1399
+ updateLoading(`ํŽ˜์ด์ง€ ์ค€๋น„ ์ค‘... (${p}/${pdf.numPages})`);
1400
+
1401
+ const pg = await pdf.getPage(p);
1402
+ const vp = pg.getViewport({scale: 1});
1403
+ const c = document.createElement('canvas');
1404
+ c.width = vp.width;
1405
+ c.height = vp.height;
1406
+
1407
+ await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
1408
+ pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
1409
  }
1410
+
1411
  hideLoading();
1412
  createFlipBook(pdfPages);
1413
+ currentLoadingPdfPath = null;
1414
+
1415
+ } catch(error) {
1416
+ console.error('PDF ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
1417
  hideLoading();
1418
+ showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1419
+ currentLoadingPdfPath = null;
1420
  }
1421
  }
1422
  } else {
1423
+ // ๋กœ์ปฌ ์—…๋กœ๋“œ๋œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰
1424
  createFlipBook(pages);
1425
+ currentLoadingPdfPath = null;
1426
  }
1427
  }
1428
 
1429
+ /* โ”€โ”€ ์ ์ง„์  ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์‹œ์ž‘ โ”€โ”€ */
1430
+ function startProgressiveLoadingIndicator(progress, totalPages) {
1431
+ // ์ง„ํ–‰ ์ƒํƒœ ํ‘œ์‹œ ํ™œ์„ฑํ™”
1432
+ $id('loadingPages').style.display = 'block';
1433
+ $id('loadingPagesCount').textContent = `${progress}%`;
1434
+
1435
+ // ๊ธฐ์กด ์ธํ„ฐ๋ฒŒ ์ œ๊ฑฐ
1436
+ if (pageLoadingInterval) {
1437
+ clearInterval(pageLoadingInterval);
1438
+ }
1439
+
1440
+ // ์ฃผ๊ธฐ์ ์œผ๋กœ ์บ์‹œ ์ƒํƒœ ํ™•์ธ (2์ดˆ๋งˆ๋‹ค)
1441
+ pageLoadingInterval = setInterval(async () => {
1442
+ if (!currentLoadingPdfPath) {
1443
  clearInterval(pageLoadingInterval);
1444
+ $id('loadingPages').style.display = 'none';
1445
  return;
1446
  }
1447
+
1448
  try {
1449
+ const response = await fetch(`/api/cache-status?path=${encodeURIComponent(currentLoadingPdfPath)}`);
1450
+ const status = await response.json();
1451
+
1452
+ // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1453
+ if (status.status === "completed") {
1454
  clearInterval(pageLoadingInterval);
1455
+ $id('loadingPages').style.display = 'none';
1456
+ refreshFlipBook(); // ์™„๋ฃŒ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ๊ณ ์นจ
1457
+ currentLoadingPdfPath = null;
1458
+ } else if (status.status === "processing") {
1459
+ $id('loadingPagesCount').textContent = `${status.progress}%`;
1460
  }
1461
+ } catch (e) {
1462
+ console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
1463
+ }
1464
  }, 1000);
1465
  }
1466
 
1467
+ /* โ”€โ”€ ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ โ”€โ”€ */
1468
+ async function refreshFlipBook() {
1469
+ if (!currentLoadingPdfPath || !fb) return;
1470
+
1471
+ try {
1472
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(currentLoadingPdfPath)}`);
1473
+ const cachedData = await response.json();
1474
+
1475
+ if(cachedData.status === "completed" && cachedData.pages) {
1476
+ // ๊ธฐ์กด ํ”Œ๋ฆฝ๋ถ ์ •๋ฆฌ
1477
+ fb.destroy();
1478
+ viewer.innerHTML = '';
1479
+
1480
+ // ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ์žฌ์ƒ์„ฑ
1481
+ createFlipBook(cachedData.pages);
1482
+ currentLoadingPdfPath = null;
1483
  }
1484
+ } catch (e) {
1485
+ console.error("ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜:", e);
1486
+ }
1487
  }
1488
 
1489
+ function createFlipBook(pages) {
1490
+ console.log('FlipBook ์ƒ์„ฑ ์‹œ์ž‘. ํŽ˜์ด์ง€ ์ˆ˜:', pages.length);
1491
+
1492
+ try {
1493
+ // ํ™”๋ฉด ๋น„์œจ ๊ณ„์‚ฐ
1494
+ const calculateAspectRatio = () => {
1495
+ const windowWidth = window.innerWidth;
1496
+ const windowHeight = window.innerHeight;
1497
+ const aspectRatio = windowWidth / windowHeight;
1498
+
1499
+ // ๋„ˆ๋น„ ๋˜๋Š” ๋†’์ด ๊ธฐ์ค€์œผ๋กœ ์ตœ๋Œ€ 90% ์ œํ•œ
1500
+ let width, height;
1501
+ if (aspectRatio > 1) { // ๊ฐ€๋กœ ํ™”๋ฉด
1502
+ height = Math.min(windowHeight * 0.9, windowHeight - 40);
1503
+ width = height * aspectRatio * 0.8; // ๊ฐ€๋กœ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ๊ฐ„ ์ค„์ž„
1504
+ if (width > windowWidth * 0.9) {
1505
+ width = windowWidth * 0.9;
1506
+ height = width / (aspectRatio * 0.8);
1507
+ }
1508
+ } else { // ์„ธ๋กœ ํ™”๋ฉด
1509
+ width = Math.min(windowWidth * 0.9, windowWidth - 40);
1510
+ height = width / aspectRatio * 0.9; // ์„ธ๋กœ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ๊ฐ„ ๋Š˜๋ฆผ
1511
+ if (height > windowHeight * 0.9) {
1512
+ height = windowHeight * 0.9;
1513
+ width = height * aspectRatio * 0.9;
1514
+ }
1515
  }
1516
+
1517
+ // ์ตœ์  ์‚ฌ์ด์ฆˆ ๋ฐ˜ํ™˜
1518
+ return {
1519
+ width: Math.round(width),
1520
+ height: Math.round(height)
1521
+ };
1522
  };
1523
+
1524
+ // ์ดˆ๊ธฐ ํ™”๋ฉด ๋น„์œจ ๊ณ„์‚ฐ
1525
+ const size = calculateAspectRatio();
1526
+ viewer.style.width = size.width + 'px';
1527
+ viewer.style.height = size.height + 'px';
1528
+
1529
+ // ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ •์ œ (๋นˆ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ)
1530
+ const validPages = pages.map(page => {
1531
+ // src๊ฐ€ ์—†๋Š” ํŽ˜์ด์ง€๋Š” ๋กœ๋”ฉ ์ค‘ ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด
1532
+ if (!page || !page.src) {
1533
+ return {
1534
+ src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+',
1535
+ thumb: page && page.thumb ? page.thumb : ''
1536
+ };
1537
  }
1538
+ return page;
1539
  });
1540
+
1541
+ fb = new FlipBook(viewer, {
1542
  pages: validPages,
1543
+ viewMode: 'webgl',
1544
+ autoSize: true,
1545
+ flipDuration: 800,
1546
+ backgroundColor: '#fff',
1547
+ /* ๐Ÿ”Š ๋‚ด์žฅ ์‚ฌ์šด๋“œ */
1548
+ sound: true,
1549
+ assets: {flipMp3: 'static/turnPage2.mp3', hardFlipMp3: 'static/turnPage2.mp3'},
1550
+ controlsProps: {
1551
+ enableFullscreen: true,
1552
+ enableToc: true,
1553
+ enableDownload: false,
1554
+ enablePrint: false,
1555
+ enableZoom: true,
1556
+ enableShare: false,
1557
+ enableSearch: true,
1558
+ enableAutoPlay: true,
1559
+ enableAnnotation: false,
1560
+ enableSound: true,
1561
+ enableLightbox: false,
1562
+ layout: 10, // ๋ ˆ์ด์•„์›ƒ ์˜ต์…˜
1563
+ skin: 'light', // ์Šคํ‚จ ์Šคํƒ€์ผ
1564
+ autoNavigationTime: 3600, // ์ž๋™ ๋„˜๊น€ ์‹œ๊ฐ„(์ดˆ)
1565
+ hideControls: false, // ์ปจํŠธ๋กค ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
1566
+ paddingTop: 10, // ์ƒ๋‹จ ํŒจ๋”ฉ
1567
+ paddingLeft: 10, // ์ขŒ์ธก ํŒจ๋”ฉ
1568
+ paddingRight: 10, // ์šฐ์ธก ํŒจ๋”ฉ
1569
+ paddingBottom: 10, // ํ•˜๋‹จ ํŒจ๋”ฉ
1570
+ pageTextureSize: 1024, // ํŽ˜์ด์ง€ ํ…์Šค์ฒ˜ ํฌ๊ธฐ
1571
+ thumbnails: true, // ์„ฌ๋„ค์ผ ํ™œ์„ฑํ™”
1572
+ autoHideControls: false, // ์ž๋™ ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
1573
+ controlsTimeout: 8000 // ์ปจํŠธ๋กค ํ‘œ์‹œ ์‹œ๊ฐ„ ์—ฐ์žฅ
1574
  }
1575
  });
1576
+
1577
+ // ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ FlipBook ํฌ๊ธฐ ์กฐ์ •
1578
+ window.addEventListener('resize', () => {
1579
+ if (fb) {
1580
+ const newSize = calculateAspectRatio();
1581
+ viewer.style.width = newSize.width + 'px';
1582
+ viewer.style.height = newSize.height + 'px';
1583
  fb.resize();
1584
  }
1585
  });
1586
+
1587
+ // FlipBook ์ƒ์„ฑ ํ›„ ์ปจํŠธ๋กค๋ฐ” ๊ฐ•์ œ ํ‘œ์‹œ
1588
+ setTimeout(() => {
1589
+ try {
1590
+ // ์ปจํŠธ๋กค๋ฐ” ๊ด€๋ จ ์š”์†Œ ์ฐพ๊ธฐ ๋ฐ ์Šคํƒ€์ผ ์ ์šฉ
1591
+ const menuBars = document.querySelectorAll('.flipbook-container .fb3d-menu-bar');
1592
+ if (menuBars && menuBars.length > 0) {
1593
+ menuBars.forEach(menuBar => {
1594
+ menuBar.style.display = 'block';
1595
+ menuBar.style.opacity = '1';
1596
+ menuBar.style.visibility = 'visible';
1597
+ menuBar.style.zIndex = '9999';
1598
+ });
1599
+ }
1600
+ } catch (e) {
1601
+ console.warn('์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜:', e);
1602
+ }
1603
+ }, 1000);
1604
+
1605
+ console.log('FlipBook ์ƒ์„ฑ ์™„๋ฃŒ');
1606
+ } catch (error) {
1607
+ console.error('FlipBook ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
1608
+ showError("FlipBook์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1609
  }
1610
  }
1611
 
1612
+ /* โ”€โ”€ ๋„ค๋น„๊ฒŒ์ด์…˜ โ”€โ”€ */
1613
  $id('homeButton').onclick=()=>{
1614
+ if(fb) {
1615
+ fb.destroy();
1616
+ viewer.innerHTML = '';
1617
+ fb = null;
1618
+ }
1619
  toggle(true);
1620
+
1621
+ // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ •๋ฆฌ
1622
+ if (pageLoadingInterval) {
1623
+ clearInterval(pageLoadingInterval);
1624
+ pageLoadingInterval = null;
1625
+ }
1626
+ $id('loadingPages').style.display = 'none';
1627
+ currentLoadingPdfPath = null;
1628
  };
1629
 
1630
  function toggle(showHome){
1631
+ $id('home').style.display=showHome?'block':'none';
1632
+ $id('viewerPage').style.display=showHome?'none':'block';
1633
+ $id('homeButton').style.display=showHome?'none':'block';
1634
+
1635
+ // ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
1636
+ if(!showHome) {
1637
+ document.body.classList.add('viewer-mode');
1638
+ } else {
1639
+ document.body.classList.remove('viewer-mode');
1640
+ }
1641
  }
1642
 
1643
+ /* -- ๋กœ๋”ฉ ๋ฐ ์˜ค๋ฅ˜ ํ‘œ์‹œ -- */
1644
+ function showLoading(message, progress = -1) {
1645
+ // ๊ธฐ์กด ๋กœ๋”ฉ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๏ฟฝ๏ฟฝ
1646
  hideLoading();
1647
+
1648
+ const loadingContainer = document.createElement('div');
1649
+ loadingContainer.className = 'loading-container fade-in';
1650
+ loadingContainer.id = 'loadingContainer';
1651
+
1652
+ let progressBarHtml = '';
1653
+ if (progress >= 0) {
1654
+ progressBarHtml = `
1655
  <div class="progress-bar-container">
1656
+ <div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>
1657
  </div>
1658
  `;
1659
  }
1660
+
1661
+ loadingContainer.innerHTML = `
1662
  <div class="loading-spinner"></div>
1663
+ <p class="loading-text" id="loadingText">${message || '๋กœ๋”ฉ ์ค‘...'}</p>
1664
+ ${progressBarHtml}
1665
  `;
1666
+
1667
+ document.body.appendChild(loadingContainer);
1668
  }
1669
+
1670
+ function updateLoading(message, progress = -1) {
1671
+ const loadingText = $id('loadingText');
1672
+ if (loadingText) {
1673
+ loadingText.textContent = message;
1674
+ }
1675
+
1676
+ if (progress >= 0) {
1677
+ let progressBar = $id('progressBar');
1678
+
1679
+ if (!progressBar) {
1680
+ const loadingContainer = $id('loadingContainer');
1681
+ if (loadingContainer) {
1682
+ const progressContainer = document.createElement('div');
1683
+ progressContainer.className = 'progress-bar-container';
1684
+ progressContainer.innerHTML = `<div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>`;
1685
+ loadingContainer.appendChild(progressContainer);
1686
+ progressBar = $id('progressBar');
1687
  }
1688
  } else {
1689
+ progressBar.style.width = `${progress}%`;
1690
  }
1691
  }
1692
  }
1693
+
1694
+ function hideLoading() {
1695
+ const loadingContainer = $id('loadingContainer');
1696
+ if (loadingContainer) {
1697
+ loadingContainer.remove();
1698
+ }
1699
  }
1700
+
1701
+ function showError(message) {
1702
+ // ๊ธฐ์กด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
1703
+ const existingError = $id('errorContainer');
1704
+ if (existingError) {
1705
+ existingError.remove();
1706
+ }
1707
+
1708
+ const errorContainer = document.createElement('div');
1709
+ errorContainer.className = 'loading-container fade-in';
1710
+ errorContainer.id = 'errorContainer';
1711
+ errorContainer.innerHTML = `
1712
+ <p class="loading-text" style="color: #e74c3c;">${message}</p>
1713
+ <button id="errorCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">ํ™•์ธ</button>
1714
  `;
1715
+
1716
+ document.body.appendChild(errorContainer);
1717
+
1718
+ // ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
1719
+ $id('errorCloseBtn').onclick = () => {
1720
+ errorContainer.remove();
1721
+ };
1722
+
1723
+ // 5์ดˆ ํ›„ ์ž๋™์œผ๋กœ ๋‹ซ๊ธฐ
1724
+ setTimeout(() => {
1725
+ if ($id('errorContainer')) {
1726
+ $id('errorContainer').remove();
1727
+ }
1728
+ }, 5000);
1729
  }
1730
 
1731
+
1732
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
1733
+ window.addEventListener('DOMContentLoaded', () => {
1734
+ console.log('DOM ๋กœ๋“œ๋จ');
1735
+
1736
+ // ์ง์ ‘ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ • (๋” ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•)
1737
  setupDirectEvents();
1738
+
1739
+ // ์•ฝ๊ฐ„์˜ ์ง€์—ฐ ํ›„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ด๋ฒคํŠธ ์„ค์ • (DOM ์™„์ „ ๋กœ๋“œ ๋ณด์žฅ)
1740
+ setTimeout(() => {
1741
+ console.log('์ง€์—ฐ ํ›„ ์ด๋ฒคํŠธ ์žฌ์„ค์ •');
1742
+ setupDirectEvents();
1743
+ }, 500);
1744
+
1745
  loadServerPDFs();
1746
+
1747
+ // ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ (3์ดˆ๋งˆ๋‹ค)
1748
+ setInterval(checkCacheStatus, 3000);
1749
  });
1750
+
1751
+
1752
  </script>
1753
  </body>
1754
  </html>
 
1759
  return get_html_content()
1760
 
1761
  if __name__ == "__main__":
1762
+ uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))