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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +513 -973
app.py CHANGED
@@ -31,11 +31,27 @@ pdf_cache: Dict[str, Dict[str, Any]] = {}
31
  # ์บ์‹ฑ ๋ฝ
32
  cache_locks = {}
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  # PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
35
  def get_pdf_files():
36
  pdf_files = []
37
  if PDF_DIR.exists():
38
- pdf_files = [f for f in PDF_DIR.glob("*.pdf")]
 
39
  return pdf_files
40
 
41
  # PDF ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
@@ -45,43 +61,40 @@ def generate_pdf_projects():
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,44 +108,33 @@ async def cache_pdf(pdf_path: str):
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,54 +142,38 @@ async def cache_pdf(pdf_path: str):
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,14 +181,12 @@ async def cache_pdf(pdf_path: str):
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()}")
@@ -215,7 +199,7 @@ async def init_cache_all_pdfs():
215
  logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
216
  pdf_files = get_pdf_files()
217
 
218
- # ์ด๋ฏธ ์บ์‹œ๋œ PDF ํŒŒ์ผ ๋กœ๋“œ (๋น ๋ฅธ ์‹œ์ž‘์„ ์œ„ํ•ด ๋จผ์ € ์ˆ˜ํ–‰)
219
  for cache_file in CACHE_DIR.glob("*_cache.json"):
220
  try:
221
  pdf_name = cache_file.stem.replace("_cache", "")
@@ -228,139 +212,126 @@ async def init_cache_all_pdfs():
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
  return {
314
- "status": "processing",
315
  "progress": progress,
316
  "pages": pages,
317
  "total_pages": total_pages,
318
  "available_pages": len([p for p in pages if p and p.get("src")])
319
  }
320
 
321
- # ์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
322
  background_tasks.add_task(cache_pdf, path)
323
  return {"status": "started", "progress": 0}
324
-
325
  except Exception as e:
326
  logger.error(f"์บ์‹œ๋œ PDF ์ œ๊ณต ์˜ค๋ฅ˜: {str(e)}")
327
  return {"error": str(e), "status": "error"}
328
 
329
- # API ์—”๋“œํฌ์ธํŠธ: PDF ์›๋ณธ ์ฝ˜ํ…์ธ  ์ œ๊ณต(์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)
330
  @app.get("/api/pdf-content")
331
  async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
332
  try:
333
- # ์บ์‹ฑ ์ƒํƒœ ํ™•์ธ
334
- pdf_file = pathlib.Path(path)
335
- if not pdf_file.exists():
336
- return JSONResponse(content={"error": f"ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {path}"}, status_code=404)
337
-
338
  pdf_name = pdf_file.stem
339
 
340
- # ์บ์‹œ๋œ ๊ฒฝ์šฐ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
341
- if pdf_name in pdf_cache and (pdf_cache[pdf_name].get("status") == "completed"
342
- or (pdf_cache[pdf_name].get("status") == "processing"
343
- and pdf_cache[pdf_name].get("progress", 0) > 10)):
 
 
 
 
344
  return JSONResponse(content={"redirect": f"/api/cached-pdf?path={path}"})
345
 
346
- # ํŒŒ์ผ ์ฝ๊ธฐ
347
- with open(path, "rb") as pdf_file:
348
- content = pdf_file.read()
349
-
350
- # ํŒŒ์ผ๋ช… ์ฒ˜๋ฆฌ
351
  import urllib.parse
352
  filename = pdf_file.name
353
  encoded_filename = urllib.parse.quote(filename)
354
 
355
- # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
356
  background_tasks.add_task(cache_pdf, path)
357
 
358
- # ์‘๋‹ต ํ—ค๋” ์„ค์ •
359
  headers = {
360
  "Content-Type": "application/pdf",
361
  "Content-Disposition": f"inline; filename=\"{encoded_filename}\"; filename*=UTF-8''{encoded_filename}"
362
  }
363
-
364
  return Response(content=content, media_type="application/pdf", headers=headers)
365
  except Exception as e:
366
  import traceback
@@ -368,16 +339,19 @@ async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
368
  logger.error(f"PDF ์ฝ˜ํ…์ธ  ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
369
  return JSONResponse(content={"error": str(e)}, status_code=500)
370
 
371
- # HTML ํŒŒ์ผ ์ฝ๊ธฐ ํ•จ์ˆ˜
 
372
  def get_html_content():
373
  html_path = BASE / "flipbook_template.html"
374
  if html_path.exists():
375
  with open(html_path, "r", encoding="utf-8") as f:
376
  return f.read()
377
- return HTML # ๊ธฐ๋ณธ HTML ์‚ฌ์šฉ
378
 
379
- # HTML ๋ฌธ์ž์—ด (UI ์ˆ˜์ • ๋ฒ„์ „)
380
- HTML = """
 
 
381
  <!doctype html>
382
  <html lang="ko">
383
  <head>
@@ -385,498 +359,241 @@ HTML = """
385
  <title>FlipBook Space</title>
386
  <link rel="stylesheet" href="/static/flipbook.css">
387
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
 
388
  <script src="/static/three.js"></script>
389
  <script src="/static/iscroll.js"></script>
390
  <script src="/static/mark.js"></script>
391
  <script src="/static/mod3d.js"></script>
392
  <script src="/static/pdf.js"></script>
 
 
 
 
 
 
 
 
 
393
  <script src="/static/flipbook.js"></script>
394
  <script src="/static/flipbook.book3.js"></script>
395
  <script src="/static/flipbook.scroll.js"></script>
396
  <script src="/static/flipbook.swipe.js"></script>
397
  <script src="/static/flipbook.webgl.js"></script>
 
398
  <style>
399
- /* ์ „์ฒด ์‚ฌ์ดํŠธ ํŒŒ์Šคํ…”ํ†ค ํ…Œ๋งˆ */
400
- :root {
401
- --primary-color: #a5d8ff; /* ํŒŒ์Šคํ…” ๋ธ”๋ฃจ */
402
- --secondary-color: #ffd6e0; /* ํŒŒ์Šคํ…” ํ•‘ํฌ */
403
- --tertiary-color: #c3fae8; /* ํŒŒ์Šคํ…” ๋ฏผํŠธ */
404
- --accent-color: #d0bfff; /* ํŒŒ์Šคํ…” ํผํ”Œ */
405
- --bg-color: #f8f9fa; /* ๋ฐ์€ ๋ฐฐ๊ฒฝ */
406
- --text-color: #495057; /* ๋ถ€๋“œ๋Ÿฌ์šด ์–ด๋‘์šด ์ƒ‰ */
407
- --card-bg: #ffffff; /* ์นด๋“œ ๋ฐฐ๊ฒฝ์ƒ‰ */
408
- --shadow-sm: 0 2px 8px rgba(0,0,0,0.05);
409
- --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
410
- --shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
411
- --radius-sm: 8px;
412
- --radius-md: 12px;
413
- --radius-lg: 16px;
414
- --transition: all 0.3s ease;
415
- }
416
-
417
  body {
418
  margin: 0;
419
- /* โ–ผ ๊ณ ๊ธ‰์ง„ ๋ธ”๋ฃจ ๊ณ„์—ด ๊ทธ๋ผ๋””์—์ด์…˜์œผ๋กœ ๋ณ€๊ฒฝ */
420
  background-image: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%);
421
  background-attachment: fixed;
422
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
423
- color: var(--text-color);
424
  }
425
-
426
- /* ํ—ค๋” ์ œ๋ชฉ ์ œ๊ฑฐ ๋ฐ Home ๋ฒ„ํŠผ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ */
427
  .floating-home {
428
- position: fixed;
429
- top: 20px;
430
- left: 20px;
431
- width: 60px;
432
- height: 60px;
433
- border-radius: 50%;
434
  background: rgba(255, 255, 255, 0.9);
435
  backdrop-filter: blur(10px);
436
- box-shadow: var(--shadow-md);
437
  z-index: 9999;
438
- display: flex;
439
- justify-content: center;
440
- align-items: center;
441
- cursor: pointer;
442
- transition: var(--transition);
443
- overflow: hidden;
444
- }
445
-
446
- .floating-home:hover {
447
- transform: scale(1.05);
448
- box-shadow: var(--shadow-lg);
449
  }
450
-
451
  .floating-home .icon {
452
- display: flex;
453
- justify-content: center;
454
- align-items: center;
455
- width: 100%;
456
- height: 100%;
457
- font-size: 22px;
458
- color: var(--primary-color);
459
- transition: var(--transition);
460
- }
461
-
462
- .floating-home:hover .icon {
463
- color: #8bc5f8;
464
  }
465
-
466
  .floating-home .title {
467
- position: absolute;
468
- left: 70px;
469
  background: rgba(255, 255, 255, 0.95);
470
- padding: 8px 20px;
471
- border-radius: 20px;
472
- box-shadow: var(--shadow-sm);
473
- font-weight: 600;
474
- font-size: 14px;
475
- white-space: nowrap;
476
- pointer-events: none;
477
- opacity: 0;
478
- transform: translateX(-10px);
479
- transition: all 0.3s ease;
480
  }
481
-
482
  .floating-home:hover .title {
483
- opacity: 1;
484
- transform: translateX(0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
486
-
487
  #home, #viewerPage {
488
- padding-top: 100px;
489
- max-width: 1200px;
490
- margin: 0 auto;
491
- padding-bottom: 60px;
492
- padding-left: 30px;
493
- padding-right: 30px;
494
- position: relative;
495
  }
496
-
497
- /* ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
498
  .upload-container {
499
- display: flex;
500
- margin-bottom: 30px;
501
- justify-content: center;
502
  }
503
-
504
  button.upload {
505
- all: unset;
506
- cursor: pointer;
507
- padding: 12px 20px;
508
- border-radius: var(--radius-md);
509
- background: white;
510
- margin: 0 10px;
511
- font-weight: 500;
512
- display: inline-flex;
513
- align-items: center;
514
- box-shadow: var(--shadow-sm);
515
- transition: var(--transition);
516
- position: relative;
517
- overflow: hidden;
518
  }
519
-
520
  button.upload::before {
521
- content: '';
522
- position: absolute;
523
- top: 0;
524
- left: 0;
525
- width: 100%;
526
- height: 100%;
527
- background: linear-gradient(120deg, var(--primary-color), var(--secondary-color));
528
- opacity: 0.08;
529
- z-index: -1;
530
  }
531
-
532
  button.upload:hover {
533
  transform: translateY(-3px);
534
- box-shadow: var(--shadow-md);
535
- }
536
-
537
- button.upload:hover::before {
538
- opacity: 0.15;
539
  }
 
 
540
 
541
- button.upload i {
542
- margin-right: 8px;
543
- font-size: 20px;
544
  }
545
-
546
- /* ๊ทธ๋ฆฌ๋“œ ๋ฐ ์นด๋“œ ์Šคํƒ€์ผ */
547
  .grid {
548
- display: grid;
549
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
550
- gap: 24px;
551
- margin-top: 36px;
 
552
  }
553
-
554
  .card {
555
- background: var(--card-bg);
556
- border-radius: var(--radius-md);
557
- cursor: pointer;
558
- box-shadow: var(--shadow-sm);
559
- width: 100%;
560
- height: 280px;
561
- position: relative;
562
- display: flex;
563
- flex-direction: column;
564
- align-items: center;
565
- justify-content: center;
566
- transition: var(--transition);
567
- overflow: hidden;
568
  }
569
-
570
  .card::before {
571
- content: '';
572
- position: absolute;
573
- top: 0;
574
- left: 0;
575
- width: 100%;
576
- height: 100%;
577
- background: linear-gradient(135deg, var(--secondary-color) 0%, var(--primary-color) 100%);
578
- opacity: 0.06;
579
- z-index: 1;
580
  }
581
-
582
  .card::after {
583
- content: '';
584
- position: absolute;
585
- top: 0;
586
- left: 0;
587
- width: 100%;
588
- height: 30%;
589
  background: linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
590
- z-index: 2;
591
  }
592
-
593
  .card img {
594
- width: 65%;
595
- height: auto;
596
- object-fit: contain;
597
- position: absolute;
598
- top: 50%;
599
- left: 50%;
600
- transform: translate(-50%, -65%);
601
- border: 1px solid rgba(0,0,0,0.05);
602
- box-shadow: 0 4px 15px rgba(0,0,0,0.08);
603
- z-index: 3;
604
- transition: var(--transition);
605
  }
606
-
607
  .card:hover {
608
  transform: translateY(-5px);
609
- box-shadow: var(--shadow-md);
610
  }
611
-
612
  .card:hover img {
613
  transform: translate(-50%, -65%) scale(1.03);
614
  box-shadow: 0 8px 20px rgba(0,0,0,0.12);
615
  }
616
-
617
  .card p {
618
- position: absolute;
619
- bottom: 20px;
620
- left: 50%;
621
- transform: translateX(-50%);
622
- background: rgba(255, 255, 255, 0.9);
623
- padding: 8px 16px;
624
- border-radius: 30px;
625
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
626
- width: 80%;
627
- text-align: center;
628
- white-space: nowrap;
629
- overflow: hidden;
630
- text-overflow: ellipsis;
631
- font-size: 14px;
632
- font-weight: 500;
633
- color: var(--text-color);
634
- z-index: 4;
635
- transition: var(--transition);
636
  }
637
-
638
  .card:hover p {
639
- background: rgba(255, 255, 255, 0.95);
640
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
641
  }
642
-
643
- /* ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ */
644
  .cached-status {
645
- position: absolute;
646
- top: 10px;
647
- right: 10px;
648
- background: var(--accent-color);
649
- color: white;
650
- font-size: 11px;
651
- padding: 3px 8px;
652
- border-radius: 12px;
653
- z-index: 5;
654
- box-shadow: var(--shadow-sm);
655
  }
656
-
657
- /* ๋ทฐ์–ด ์Šคํƒ€์ผ */
658
  #viewer {
659
- width: 90%;
660
- height: 90vh;
661
- max-width: 90%;
662
- margin: 0;
663
- background: var(--card-bg);
664
- border: none;
665
- border-radius: var(--radius-lg);
666
- position: fixed;
667
- top: 50%;
668
- left: 50%;
669
- transform: translate(-50%, -50%);
670
- z-index: 1000;
671
- box-shadow: var(--shadow-lg);
672
- max-height: calc(90vh - 40px);
673
- aspect-ratio: auto;
674
- object-fit: contain;
675
- overflow: hidden;
676
  }
677
-
678
- /* FlipBook ์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ */
679
  .flipbook-container .fb3d-menu-bar {
680
- z-index: 2000 !important;
681
- opacity: 1 !important;
682
- bottom: 0 !important;
683
- background-color: rgba(255,255,255,0.9) !important;
684
- backdrop-filter: blur(10px) !important;
685
- border-radius: 0 0 var(--radius-lg) var(--radius-lg) !important;
686
- padding: 12px 0 !important;
687
- box-shadow: 0 -4px 20px rgba(0,0,0,0.1) !important;
688
  }
689
-
690
  .flipbook-container .fb3d-menu-bar > ul > li > img,
691
  .flipbook-container .fb3d-menu-bar > ul > li > div {
692
- opacity: 1 !important;
693
- transform: scale(1.2) !important;
694
- filter: drop-shadow(0 2px 3px rgba(0,0,0,0.1)) !important;
695
- }
696
-
697
- .flipbook-container .fb3d-menu-bar > ul > li {
698
- margin: 0 12px !important;
699
  }
700
-
701
- /* ๋ฉ”๋‰ด ํˆดํŒ ์Šคํƒ€์ผ */
702
  .flipbook-container .fb3d-menu-bar > ul > li > span {
703
- background-color: rgba(0,0,0,0.7) !important;
704
- color: white !important;
705
- border-radius: var(--radius-sm) !important;
706
- padding: 8px 12px !important;
707
- font-size: 13px !important;
708
- bottom: 55px !important;
709
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
710
- letter-spacing: 0.3px !important;
711
  }
712
-
713
- /* ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ๋ฐฐ๊ฒฝ ์˜ค๋ฒ„๋ ˆ์ด */
714
  .viewer-mode {
715
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
716
  }
717
-
718
- /* ๋ทฐ์–ด ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ */
719
  #viewerPage {
720
- background: transparent;
721
  }
722
-
723
- /* ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
724
- @keyframes spin {
725
- 0% { transform: rotate(0deg); }
726
- 100% { transform: rotate(360deg); }
727
- }
728
-
729
  .loading-spinner {
730
- border: 4px solid rgba(255,255,255,0.3);
731
- border-top: 4px solid var(--primary-color);
732
- border-radius: 50%;
733
- width: 50px;
734
- height: 50px;
735
- margin: 0 auto;
736
- animation: spin 1.5s ease-in-out infinite;
737
  }
738
-
739
  .loading-container {
740
- position: absolute;
741
- top: 50%;
742
- left: 50%;
743
- transform: translate(-50%, -50%);
744
- text-align: center;
745
- background: rgba(255, 255, 255, 0.85);
746
- backdrop-filter: blur(10px);
747
- padding: 30px;
748
- border-radius: var(--radius-md);
749
- box-shadow: var(--shadow-md);
750
- z-index: 9999;
751
  }
752
-
753
  .loading-text {
754
- margin-top: 20px;
755
- font-size: 16px;
756
- color: var(--text-color);
757
- font-weight: 500;
758
- }
759
-
760
- /* ํŽ˜์ด์ง€ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
761
- @keyframes fadeIn {
762
- from { opacity: 0; }
763
- to { opacity: 1; }
764
- }
765
-
766
- .fade-in {
767
- animation: fadeIn 0.5s ease-out;
768
- }
769
-
770
- /* ์ถ”๊ฐ€ ์Šคํƒ€์ผ */
771
- .section-title {
772
- font-size: 1.3rem;
773
- font-weight: 600;
774
- margin: 30px 0 15px;
775
- color: var(--text-color);
776
  }
777
-
778
- .no-projects {
779
- text-align: center;
780
- margin: 40px 0;
781
- color: var(--text-color);
782
- font-size: 16px;
783
- }
784
-
785
- /* ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” */
786
  .progress-bar-container {
787
- width: 100%;
788
- height: 6px;
789
- background-color: rgba(0,0,0,0.1);
790
- border-radius: 3px;
791
- margin-top: 15px;
792
- overflow: hidden;
793
  }
794
-
795
  .progress-bar {
796
- height: 100%;
797
- background: linear-gradient(to right, var(--primary-color), var(--accent-color));
798
- border-radius: 3px;
799
- transition: width 0.3s ease;
800
  }
801
-
802
- /* ํ—ค๋” ๋กœ๊ณ  ๋ฐ ํƒ€์ดํ‹€ */
803
- /* โ–ผ pointer-events: none ์ œ๊ฑฐ, ๋†’์ด์™€ ํŒจ๋”ฉ ์กฐ์ •ํ•˜์—ฌ ์ œ๋ชฉ์„ ๋” ์œ„๋กœ */
804
- .library-header {
805
- position: fixed;
806
- top: 10px; /* ๊ธฐ์กด 20px -> 10px์œผ๋กœ */
807
- left: 0;
808
- right: 0;
809
- text-align: center;
810
- z-index: 100;
811
- }
812
-
813
- .library-header .title {
814
- display: inline-block;
815
- /* ๋†’์ด ์ค„์ด๊ธฐ ์œ„ํ•œ ํŒจ๋”ฉ/ํฐํŠธ ํฌ๊ธฐ ์กฐ์ • */
816
- padding: 8px 20px; /* ๊ธฐ์กด 12px 30px -> 8px 20px */
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.2rem; /* ๊ธฐ์กด 1.5rem -> 1.2rem */
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
- }
828
-
829
- /* ์ ์ง„์  ๋กœ๋”ฉ ํ‘œ์‹œ */
830
  .loading-pages {
831
- position: absolute;
832
- bottom: 20px;
833
- left: 50%;
834
- transform: translateX(-50%);
835
- background: rgba(255, 255, 255, 0.9);
836
- padding: 10px 20px;
837
- border-radius: 20px;
838
- box-shadow: var(--shadow-md);
839
- font-size: 14px;
840
- color: var(--text-color);
841
- z-index: 9998;
842
- text-align: center;
843
  }
844
-
845
- /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
846
- @media (max-width: 768px) {
847
- .grid {
848
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
849
- gap: 16px;
850
- }
851
-
852
- .card {
853
- height: 240px;
854
- }
855
-
856
- .library-header .title {
857
- font-size: 1rem;
858
- padding: 6px 16px;
859
- }
860
-
861
- .floating-home {
862
- width: 50px;
863
- height: 50px;
864
- }
865
-
866
- .floating-home .icon {
867
- font-size: 18px;
868
- }
869
  }
870
  </style>
871
  </head>
872
  <body>
873
- <!-- ์ œ๋ชฉ์„ Home ๋ฒ„ํŠผ๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์–ด๋กœ ์ฒ˜๋ฆฌ -->
874
  <div id="homeButton" class="floating-home" style="display:none;">
875
  <div class="icon"><i class="fas fa-home"></i></div>
876
  <div class="title">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</div>
877
  </div>
878
-
879
- <!-- ์„ผํ„ฐ ์ •๋ ฌ๋œ ํƒ€์ดํ‹€ -->
880
  <div class="library-header">
881
  <div class="title">FlipBook Library</div>
882
  </div>
@@ -894,139 +611,107 @@ HTML = """
894
  </div>
895
 
896
  <div class="section-title">๋‚ด ํ”„๋กœ์ ํŠธ</div>
897
- <div class="grid" id="grid">
898
- <!-- ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
899
- </div>
900
- <div id="noProjects" class="no-projects" style="display: none;">
901
  ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๋‚˜ PDF๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
902
  </div>
903
  </section>
904
 
905
- <section id="viewerPage" style="display:none">
906
  <div id="viewer"></div>
907
- <div id="loadingPages" class="loading-pages" style="display:none;">ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0/0</span></div>
 
 
908
  </section>
909
 
910
  <script>
911
- let projects=[], fb=null;
912
- const grid=$id('grid'), viewer=$id('viewer');
913
- pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
914
-
915
- // ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
916
  let serverProjects = [];
917
-
918
- // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ
919
  let currentLoadingPdfPath = null;
920
  let pageLoadingInterval = null;
921
 
922
- /* ๐Ÿ”Š ์˜ค๋””์˜ค unlock โ€“ ๋‚ด์žฅ Audio ์™€ ๊ฐ™์€ MP3 ๊ฒฝ๋กœ ์‚ฌ์šฉ */
 
923
  ['click','touchstart'].forEach(evt=>{
924
- document.addEventListener(evt,function u(){new Audio('static/turnPage2.mp3')
925
- .play().then(a=>a.pause()).catch(()=>{});document.removeEventListener(evt,u,{capture:true});},
926
- {once:true,capture:true});
 
927
  });
928
-
929
- /* โ”€โ”€ ์œ ํ‹ธ โ”€โ”€ */
930
- function $id(id){return document.getElementById(id)}
931
-
932
- // ์ง์ ‘ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ • (์—…๋กœ๋“œ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ํŒŒ์ผ ์„ ํƒ์ฐฝ ์—ด๊ธฐ)
933
  function setupDirectEvents() {
934
- // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ
935
  const imageBtn = $id('imageUploadBtn');
936
  const imageInput = $id('imgInput');
937
  if (imageBtn && imageInput) {
938
- console.log('์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •');
939
- imageBtn.onclick = function(e) {
940
- e.preventDefault();
941
- e.stopPropagation();
942
  imageInput.click();
943
  };
944
  }
945
-
946
- // PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ
947
  const pdfBtn = $id('pdfUploadBtn');
948
  const pdfInput = $id('pdfInput');
949
  if (pdfBtn && pdfInput) {
950
- console.log('PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •');
951
- pdfBtn.onclick = function(e) {
952
- e.preventDefault();
953
- e.stopPropagation();
954
  pdfInput.click();
955
  };
956
  }
957
  }
958
 
959
- function addCard(i, thumb, title, isCached = false) {
960
  const d = document.createElement('div');
961
  d.className = 'card fade-in';
962
- d.onclick = () => open(i);
963
-
964
- const displayTitle = title ?
965
- (title.length > 15 ? title.substring(0, 15) + '...' : title) :
966
- 'ํ”„๋กœ์ ํŠธ ' + (i+1);
967
-
968
- const cachedBadge = isCached ?
969
- '<div class="cached-status">์บ์‹œ๋จ</div>' : '';
970
-
971
  d.innerHTML = `
972
  <div class="card-inner">
973
  ${cachedBadge}
974
  <img src="${thumb}" alt="${displayTitle}" loading="lazy">
975
- <p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
976
  </div>
977
  `;
978
- grid.appendChild(d);
979
-
980
- // ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์œผ๋ฉด 'ํ”„๋กœ์ ํŠธ ์—†์Œ' ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
981
  $id('noProjects').style.display = 'none';
982
  }
983
 
984
- /* โ”€โ”€ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ โ”€โ”€ */
985
  $id('imgInput').onchange = e => {
986
- const files = [...e.target.files];
987
- if(!files.length) return;
988
-
989
  showLoading("์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘...");
990
-
991
- const pages=[], tot = files.length;
992
- let done = 0;
993
- files.forEach((f,i)=> {
994
  const r=new FileReader();
995
  r.onload=x=>{
996
- pages[i] = {src: x.target.result, thumb: x.target.result};
997
- if(++done===tot) {
998
- save(pages, '์ด๋ฏธ์ง€ ์ปฌ๋ ‰์…˜');
999
- hideLoading();
1000
- }
1001
  };
1002
  r.readAsDataURL(f);
1003
  });
1004
  };
1005
 
1006
- /* โ”€โ”€ PDF ์—…๋กœ๋“œ โ”€โ”€ */
1007
  $id('pdfInput').onchange = e => {
1008
- const file = e.target.files[0];
1009
- if(!file) return;
1010
-
1011
  showLoading("PDF ๋กœ๋”ฉ ์ค‘...");
1012
-
1013
- const fr = new FileReader();
1014
- fr.onload = v => {
1015
- pdfjsLib.getDocument({data: v.target.result}).promise.then(async pdf=>{
1016
- const pages = [];
1017
-
1018
- for(let p=1; p <= pdf.numPages; p++){
 
 
 
1019
  updateLoading(\`PDF ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... (\${p}/\${pdf.numPages})\`);
1020
  const pg=await pdf.getPage(p), vp=pg.getViewport({scale:1});
1021
- const c=document.createElement('canvas');
1022
- c.width=vp.width;c.height=vp.height;
1023
  await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
1024
  pages.push({src:c.toDataURL(), thumb:c.toDataURL()});
1025
  }
1026
  hideLoading();
1027
- save(pages, file.name.replace('.pdf', ''));
1028
- }).catch(error => {
1029
- console.error("PDF ๋กœ๋”ฉ ์˜ค๋ฅ˜:", error);
1030
  hideLoading();
1031
  showError("PDF ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1032
  });
@@ -1034,527 +719,382 @@ HTML = """
1034
  fr.readAsArrayBuffer(file);
1035
  };
1036
 
1037
- function save(pages, title, isCached = false){
1038
  const id=projects.push(pages)-1;
1039
  addCard(id, pages[0].thumb, title, isCached);
1040
  }
1041
 
1042
- /* โ”€โ”€ ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ โ”€โ”€ */
1043
  async function loadServerPDFs() {
1044
  try {
1045
- if (document.querySelectorAll('.card').length === 0) {
1046
  showLoading("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘...");
1047
  }
1048
-
1049
- // ๋จผ์ € ์บ์‹œ ์ƒํƒœ ํ™•์ธ
1050
- const cacheStatusRes = await fetch('/api/cache-status');
1051
- const cacheStatus = await cacheStatusRes.json();
1052
-
1053
- // PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
1054
- const response = await fetch('/api/pdf-projects');
1055
- serverProjects = await response.json();
1056
-
1057
- if (serverProjects.length === 0) {
1058
  hideLoading();
1059
- $id('noProjects').style.display = 'block';
1060
  return;
1061
  }
1062
-
1063
- const thumbnailPromises = serverProjects.map(async (project, index) => {
1064
- updateLoading(\`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (\${index+1}/\${serverProjects.length})\`);
1065
-
1066
- const pdfName = project.name;
1067
  const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
1068
-
1069
  try {
1070
- // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
1071
- const response = await fetch(\`/api/pdf-thumbnail?path=\${encodeURIComponent(project.path)}\`);
1072
- const data = await response.json();
1073
-
1074
- if(data.thumbnail) {
1075
- const pages = [{
1076
- src: data.thumbnail,
1077
- thumb: data.thumbnail,
1078
- path: project.path,
1079
- cached: isCached
1080
- }];
1081
- return { pages, name: project.name, isCached };
 
1082
  }
1083
- } catch (err) {
1084
- console.error(\`์ธ๋„ค์ผ ๋กœ๋“œ ์˜ค๋ฅ˜ (\${project.name}):\`, err);
1085
  }
1086
-
1087
  return null;
1088
  });
1089
-
1090
- const results = await Promise.all(thumbnailPromises);
1091
-
1092
- results.filter(result => result !== null).forEach(result => {
1093
- save(result.pages, result.name, result.isCached);
1094
  });
1095
-
1096
  hideLoading();
1097
-
1098
- if (document.querySelectorAll('.card').length === 0) {
1099
- $id('noProjects').style.display = 'block';
1100
  }
1101
- } catch(error) {
1102
- console.error('์„œ๋ฒ„ PDF ๋กœ๋“œ ์‹คํŒจ:', error);
1103
  hideLoading();
1104
  showError("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1105
  }
1106
  }
1107
 
1108
- /* โ”€โ”€ ์บ์‹œ ์ƒํƒœ ์ •๊ธฐ์ ์œผ๋กœ ํ™•์ธ โ”€โ”€ */
1109
- async function checkCacheStatus() {
1110
  try {
1111
- const response = await fetch('/api/cache-status');
1112
- const cacheStatus = await response.json();
1113
-
1114
- // ํ˜„์žฌ ์นด๋“œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1115
- const cards = document.querySelectorAll('.card');
1116
-
1117
- for(let i = 0; i < cards.length; i++) {
1118
- if(projects[i] && projects[i][0] && projects[i][0].path) {
1119
- const pdfPath = projects[i][0].path;
1120
- const pdfName = pdfPath.split('/').pop().replace('.pdf', '');
1121
-
1122
  let badgeEl = cards[i].querySelector('.cached-status');
1123
-
1124
- if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed") {
1125
- if(!badgeEl) {
1126
- badgeEl = document.createElement('div');
1127
- badgeEl.className = 'cached-status';
1128
- badgeEl.textContent = '์บ์‹œ๋จ';
1129
  cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1130
- } else if (badgeEl.textContent !== '์บ์‹œ๋จ') {
1131
- badgeEl.textContent = '์บ์‹œ๋จ';
1132
- badgeEl.style.background = 'var(--accent-color)';
1133
  }
1134
- projects[i][0].cached = true;
1135
- } else if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "processing") {
1136
- if(!badgeEl) {
1137
- badgeEl = document.createElement('div');
1138
- badgeEl.className = 'cached-status';
1139
  cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1140
  }
1141
- badgeEl.textContent = \`\${cacheStatus[pdfName].progress}%\`;
1142
- badgeEl.style.background = 'var(--secondary-color)';
1143
  }
1144
  }
1145
  }
1146
-
1147
- // ํ˜„์žฌ ๋กœ๋”ฉ ์ค‘์ธ PDF๊ฐ€ ์žˆ์œผ๋ฉด ์ƒํƒœ ํ™•์ธ
1148
- if (currentLoadingPdfPath && pageLoadingInterval) {
1149
- const pdfName = currentLoadingPdfPath.split('/').pop().replace('.pdf', '');
1150
- if (cacheStatus[pdfName]) {
1151
- const status = cacheStatus[pdfName].status;
1152
- const progress = cacheStatus[pdfName].progress || 0;
1153
-
1154
- if (status === "completed") {
1155
  clearInterval(pageLoadingInterval);
1156
- $id('loadingPages').style.display = 'none';
1157
- currentLoadingPdfPath = null;
1158
  refreshFlipBook();
1159
- } else if (status === "processing") {
1160
- $id('loadingPages').style.display = 'block';
1161
- $id('loadingPagesCount').textContent = \`\${progress}%\`;
1162
  }
1163
  }
1164
  }
1165
- } catch(error) {
1166
- console.error('์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
1167
  }
1168
  }
1169
 
1170
- /* โ”€โ”€ ์นด๋“œ โ†’ FlipBook โ”€โ”€ */
1171
- async function open(i) {
1172
  toggle(false);
1173
- const pages = projects[i];
1174
-
1175
- if(fb) {
1176
- fb.destroy();
1177
- viewer.innerHTML = '';
1178
- }
1179
-
1180
- if(pages[0].path) {
1181
  const pdfPath = pages[0].path;
1182
- let progressiveLoading = false;
1183
- currentLoadingPdfPath = pdfPath;
1184
-
1185
- if(pages[0].cached) {
1186
  showLoading("์บ์‹œ๋œ PDF ๋กœ๋”ฉ ์ค‘...");
1187
  try {
1188
- const response = await fetch(\`/api/cached-pdf?path=\${encodeURIComponent(pdfPath)}\`);
1189
- const cachedData = await response.json();
1190
-
1191
- if(cachedData.status === "completed" && cachedData.pages) {
1192
- hideLoading();
1193
- createFlipBook(cachedData.pages);
1194
- currentLoadingPdfPath = null;
1195
- return;
1196
- } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1197
- hideLoading();
1198
- createFlipBook(cachedData.pages);
1199
- progressiveLoading = true;
1200
- startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1201
  }
1202
- } catch(error) {
1203
- console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
1204
- }
1205
  }
1206
-
1207
- if(!progressiveLoading) {
1208
  showLoading("PDF ์ค€๋น„ ์ค‘...");
1209
  try {
1210
- const response = await fetch(\`/api/pdf-content?path=\${encodeURIComponent(pdfPath)}\`);
1211
- const data = await response.json();
1212
-
1213
- if(data.redirect) {
1214
- const redirectRes = await fetch(data.redirect);
1215
- const cachedData = await redirectRes.json();
1216
-
1217
- if(cachedData.status === "completed" && cachedData.pages) {
1218
- hideLoading();
1219
- createFlipBook(cachedData.pages);
1220
- currentLoadingPdfPath = null;
1221
- return;
1222
- } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1223
- hideLoading();
1224
- createFlipBook(cachedData.pages);
1225
- startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1226
  return;
1227
  }
1228
  }
1229
-
1230
- const pdfResponse = await fetch(\`/api/pdf-content?path=\${encodeURIComponent(pdfPath)}\`);
1231
-
1232
- try {
1233
- const jsonData = await pdfResponse.clone().json();
1234
- if (jsonData.redirect) {
1235
- const redirectRes = await fetch(jsonData.redirect);
1236
- const cachedData = await redirectRes.json();
1237
-
1238
- if(cachedData.pages && cachedData.pages.length > 0) {
1239
- hideLoading();
1240
- createFlipBook(cachedData.pages);
1241
- if(cachedData.status === "processing") {
1242
- startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1243
  } else {
1244
- currentLoadingPdfPath = null;
1245
  }
1246
  return;
1247
  }
1248
  }
1249
- } catch (e) {
1250
- // JSON ํŒŒ์‹ฑ ์‹คํŒจ -> PDF ์›๋ณธ ๋ฐ์ดํ„ฐ๋กœ ์ฒ˜๋ฆฌ
 
 
 
 
 
1251
  }
1252
-
1253
- const pdfData = await pdfResponse.arrayBuffer();
1254
- const pdf = await pdfjsLib.getDocument({data: pdfData}).promise;
1255
- const pdfPages = [];
1256
-
1257
- for(let p=1; p <= pdf.numPages; p++) {
1258
  updateLoading(\`ํŽ˜์ด์ง€ ์ค€๋น„ ์ค‘... (\${p}/\${pdf.numPages})\`);
1259
- const pg = await pdf.getPage(p);
1260
- const vp = pg.getViewport({scale: 1});
1261
- const c = document.createElement('canvas');
1262
- c.width = vp.width;
1263
- c.height = vp.height;
1264
- await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
1265
- pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
1266
  }
1267
  hideLoading();
1268
  createFlipBook(pdfPages);
1269
- currentLoadingPdfPath = null;
1270
- } catch(error) {
1271
- console.error('PDF ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error);
1272
  hideLoading();
1273
- showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1274
- currentLoadingPdfPath = null;
1275
  }
1276
  }
1277
  } else {
1278
  createFlipBook(pages);
1279
- currentLoadingPdfPath = null;
1280
  }
1281
  }
1282
 
1283
- function startProgressiveLoadingIndicator(progress, totalPages) {
1284
- $id('loadingPages').style.display = 'block';
1285
- $id('loadingPagesCount').textContent = \`\${progress}%\`;
1286
-
1287
- if (pageLoadingInterval) {
1288
- clearInterval(pageLoadingInterval);
1289
- }
1290
- pageLoadingInterval = setInterval(async () => {
1291
- if (!currentLoadingPdfPath) {
1292
  clearInterval(pageLoadingInterval);
1293
- $id('loadingPages').style.display = 'none';
1294
  return;
1295
  }
1296
  try {
1297
- const response = await fetch(\`/api/cache-status?path=\${encodeURIComponent(currentLoadingPdfPath)}\`);
1298
- const status = await response.json();
1299
-
1300
- if (status.status === "completed") {
1301
  clearInterval(pageLoadingInterval);
1302
- $id('loadingPages').style.display = 'none';
1303
  refreshFlipBook();
1304
- currentLoadingPdfPath = null;
1305
- } else if (status.status === "processing") {
1306
- $id('loadingPagesCount').textContent = \`\${status.progress}%\`;
1307
  }
1308
- } catch (e) {
1309
- console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
1310
- }
1311
  }, 1000);
1312
  }
1313
 
1314
- async function refreshFlipBook() {
1315
- if (!currentLoadingPdfPath || !fb) return;
1316
- try {
1317
- const response = await fetch(\`/api/cached-pdf?path=\${encodeURIComponent(currentLoadingPdfPath)}\`);
1318
- const cachedData = await response.json();
1319
-
1320
- if(cachedData.status === "completed" && cachedData.pages) {
1321
- fb.destroy();
1322
- viewer.innerHTML = '';
1323
- createFlipBook(cachedData.pages);
1324
- currentLoadingPdfPath = null;
1325
  }
1326
- } catch (e) {
1327
- console.error("ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜:", e);
1328
- }
1329
  }
1330
 
1331
- function createFlipBook(pages) {
1332
- console.log('FlipBook ์ƒ์„ฑ ์‹œ์ž‘. ํŽ˜์ด์ง€ ์ˆ˜:', pages.length);
1333
- try {
1334
- const calculateAspectRatio = () => {
1335
- const windowWidth = window.innerWidth;
1336
- const windowHeight = window.innerHeight;
1337
- const aspectRatio = windowWidth / windowHeight;
1338
-
1339
- let width, height;
1340
- if (aspectRatio > 1) {
1341
- height = Math.min(windowHeight * 0.9, windowHeight - 40);
1342
- width = height * aspectRatio * 0.8;
1343
- if (width > windowWidth * 0.9) {
1344
- width = windowWidth * 0.9;
1345
- height = width / (aspectRatio * 0.8);
1346
- }
1347
  } else {
1348
- width = Math.min(windowWidth * 0.9, windowWidth - 40);
1349
- height = width / aspectRatio * 0.9;
1350
- if (height > windowHeight * 0.9) {
1351
- height = windowHeight * 0.9;
1352
- width = height * aspectRatio * 0.9;
1353
- }
1354
  }
1355
- return {
1356
- width: Math.round(width),
1357
- height: Math.round(height)
1358
- };
1359
  };
1360
-
1361
- const size = calculateAspectRatio();
1362
- viewer.style.width = size.width + 'px';
1363
- viewer.style.height = size.height + 'px';
1364
-
1365
- const validPages = pages.map(page => {
1366
- if (!page || !page.src) {
1367
- return {
1368
- src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+',
1369
- thumb: page && page.thumb ? page.thumb : ''
1370
- };
1371
  }
1372
- return page;
1373
  });
1374
-
1375
- fb = new FlipBook(viewer, {
1376
  pages: validPages,
1377
- viewMode: 'webgl',
1378
- autoSize: true,
1379
- flipDuration: 800,
1380
- backgroundColor: '#fff',
1381
- sound: true,
1382
- assets: {flipMp3: 'static/turnPage2.mp3', hardFlipMp3: 'static/turnPage2.mp3'},
1383
- controlsProps: {
1384
- enableFullscreen: true,
1385
- enableToc: true,
1386
- enableDownload: false,
1387
- enablePrint: false,
1388
- enableZoom: true,
1389
- enableShare: false,
1390
- enableSearch: true,
1391
- enableAutoPlay: true,
1392
- enableAnnotation: false,
1393
- enableSound: true,
1394
- enableLightbox: false,
1395
- layout: 10,
1396
- skin: 'light',
1397
- autoNavigationTime: 3600,
1398
- hideControls: false,
1399
- paddingTop: 10,
1400
- paddingLeft: 10,
1401
- paddingRight: 10,
1402
- paddingBottom: 10,
1403
- pageTextureSize: 1024,
1404
- thumbnails: true,
1405
- autoHideControls: false,
1406
- controlsTimeout: 8000
1407
  }
1408
  });
1409
-
1410
- window.addEventListener('resize', () => {
1411
- if (fb) {
1412
- const newSize = calculateAspectRatio();
1413
- viewer.style.width = newSize.width + 'px';
1414
- viewer.style.height = newSize.height + 'px';
1415
  fb.resize();
1416
  }
1417
  });
1418
-
1419
- setTimeout(() => {
1420
- try {
1421
- const menuBars = document.querySelectorAll('.flipbook-container .fb3d-menu-bar');
1422
- if (menuBars && menuBars.length > 0) {
1423
- menuBars.forEach(menuBar => {
1424
- menuBar.style.display = 'block';
1425
- menuBar.style.opacity = '1';
1426
- menuBar.style.visibility = 'visible';
1427
- menuBar.style.zIndex = '9999';
1428
- });
1429
- }
1430
- } catch (e) {
1431
- console.warn('์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜:', e);
1432
- }
1433
- }, 1000);
1434
-
1435
- console.log('FlipBook ์ƒ์„ฑ ์™„๋ฃŒ');
1436
- } catch (error) {
1437
- console.error('FlipBook ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
1438
- showError("FlipBook์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1439
  }
1440
  }
1441
 
1442
- $id('homeButton').onclick = () => {
1443
- if(fb) {
1444
- fb.destroy();
1445
- viewer.innerHTML = '';
1446
- fb = null;
1447
- }
1448
  toggle(true);
1449
- if (pageLoadingInterval) {
1450
- clearInterval(pageLoadingInterval);
1451
- pageLoadingInterval = null;
1452
- }
1453
- $id('loadingPages').style.display = 'none';
1454
- currentLoadingPdfPath = null;
1455
  };
1456
 
1457
  function toggle(showHome){
1458
- $id('home').style.display = showHome?'block':'none';
1459
- $id('viewerPage').style.display = showHome?'none':'block';
1460
- $id('homeButton').style.display = showHome?'none':'block';
1461
-
1462
- if(!showHome) {
1463
- document.body.classList.add('viewer-mode');
1464
- } else {
1465
- document.body.classList.remove('viewer-mode');
1466
- }
1467
  }
1468
 
1469
- function showLoading(message, progress = -1) {
1470
  hideLoading();
1471
- const loadingContainer = document.createElement('div');
1472
- loadingContainer.className = 'loading-container fade-in';
1473
- loadingContainer.id = 'loadingContainer';
1474
-
1475
- let progressBarHtml = '';
1476
- if (progress >= 0) {
1477
- progressBarHtml = `
1478
  <div class="progress-bar-container">
1479
- <div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>
1480
  </div>
1481
  `;
1482
  }
1483
-
1484
- loadingContainer.innerHTML = `
1485
  <div class="loading-spinner"></div>
1486
- <p class="loading-text" id="loadingText">${message || '๋กœ๋”ฉ ์ค‘...'}</p>
1487
- ${progressBarHtml}
1488
  `;
1489
-
1490
- document.body.appendChild(loadingContainer);
1491
  }
1492
-
1493
- function updateLoading(message, progress = -1) {
1494
- const loadingText = $id('loadingText');
1495
- if (loadingText) {
1496
- loadingText.textContent = message;
1497
- }
1498
-
1499
- if (progress >= 0) {
1500
- let progressBar = $id('progressBar');
1501
- if (!progressBar) {
1502
- const loadingContainer = $id('loadingContainer');
1503
- if (loadingContainer) {
1504
- const progressContainer = document.createElement('div');
1505
- progressContainer.className = 'progress-bar-container';
1506
- progressContainer.innerHTML = `<div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>`;
1507
- loadingContainer.appendChild(progressContainer);
1508
- progressBar = $id('progressBar');
1509
  }
1510
  } else {
1511
- progressBar.style.width = \`\${progress}%\`;
1512
  }
1513
  }
1514
  }
1515
-
1516
- function hideLoading() {
1517
- const loadingContainer = $id('loadingContainer');
1518
- if (loadingContainer) {
1519
- loadingContainer.remove();
1520
- }
1521
  }
1522
-
1523
- function showError(message) {
1524
- const existingError = $id('errorContainer');
1525
- if (existingError) {
1526
- existingError.remove();
1527
- }
1528
-
1529
- const errorContainer = document.createElement('div');
1530
- errorContainer.className = 'loading-container fade-in';
1531
- errorContainer.id = 'errorContainer';
1532
- errorContainer.innerHTML = `
1533
- <p class="loading-text" style="color: #e74c3c;">${message}</p>
1534
- <button id="errorCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">ํ™•์ธ</button>
1535
  `;
1536
-
1537
- document.body.appendChild(errorContainer);
1538
-
1539
- $id('errorCloseBtn').onclick = () => {
1540
- errorContainer.remove();
1541
- };
1542
-
1543
- setTimeout(() => {
1544
- if ($id('errorContainer')) {
1545
- $id('errorContainer').remove();
1546
- }
1547
- }, 5000);
1548
  }
1549
 
1550
- window.addEventListener('DOMContentLoaded', () => {
1551
- // ์—…๋กœ๋“œ ๋ฒ„ํŠผ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋„๋ก ์ง์ ‘ ์ด๋ฒคํŠธ ์„ค์ •
1552
  setupDirectEvents();
1553
-
1554
  loadServerPDFs();
1555
-
1556
- // ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
1557
- setInterval(checkCacheStatus, 3000);
1558
  });
1559
  </script>
1560
  </body>
 
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
 
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
  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
  }
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
  "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()}")
 
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
  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
  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
  <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>
 
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
  });
 
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>