Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Update app.py
Browse files
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 |
-
|
|
|
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 |
-
#
|
60 |
async def cache_pdf(pdf_path: str):
|
61 |
try:
|
62 |
import fitz # PyMuPDF
|
63 |
-
|
64 |
-
|
|
|
65 |
pdf_name = pdf_file.stem
|
66 |
|
67 |
-
#
|
68 |
if pdf_name not in cache_locks:
|
69 |
cache_locks[pdf_name] = threading.Lock()
|
70 |
|
71 |
-
# ์ด๋ฏธ
|
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(
|
100 |
total_pages = doc.page_count
|
101 |
|
102 |
-
#
|
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 |
-
|
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 |
-
#
|
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 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
|
237 |
-
# ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์์ ํจ์
|
238 |
@app.on_event("startup")
|
239 |
async def startup_event():
|
240 |
-
# ๋ฐฑ๊ทธ๋ผ์ด๋ ํ์คํฌ๋ก ์บ์ฑ ์คํ
|
241 |
asyncio.create_task(init_cache_all_pdfs())
|
242 |
|
243 |
-
|
|
|
|
|
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 =
|
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 |
-
|
|
|
|
|
|
|
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)
|
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 =
|
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 {
|
290 |
-
|
|
|
|
|
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 =
|
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 |
-
|
342 |
-
|
343 |
-
|
|
|
|
|
|
|
|
|
344 |
return JSONResponse(content={"redirect": f"/api/cached-pdf?path={path}"})
|
345 |
|
346 |
-
|
347 |
-
|
348 |
-
|
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 |
-
|
|
|
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
|
378 |
|
379 |
-
|
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:
|
424 |
}
|
425 |
-
|
426 |
-
/* ํค๋ ์ ๋ชฉ ์ ๊ฑฐ ๋ฐ Home ๋ฒํผ ๋ ์ด์ด ์ฒ๋ฆฌ */
|
427 |
.floating-home {
|
428 |
-
position: fixed;
|
429 |
-
|
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:
|
437 |
z-index: 9999;
|
438 |
-
display: flex;
|
439 |
-
|
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 |
-
|
453 |
-
|
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 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
485 |
}
|
486 |
-
|
487 |
#home, #viewerPage {
|
488 |
-
padding-top: 100px;
|
489 |
-
|
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 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
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 |
-
|
523 |
-
|
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:
|
535 |
-
}
|
536 |
-
|
537 |
-
button.upload:hover::before {
|
538 |
-
opacity: 0.15;
|
539 |
}
|
|
|
|
|
540 |
|
541 |
-
|
542 |
-
|
543 |
-
font-size: 20px;
|
544 |
}
|
545 |
-
|
546 |
-
/* ๊ทธ๋ฆฌ๋ ๋ฐ ์นด๋ ์คํ์ผ */
|
547 |
.grid {
|
548 |
-
display: grid;
|
549 |
-
|
550 |
-
|
551 |
-
|
|
|
552 |
}
|
553 |
-
|
554 |
.card {
|
555 |
-
background:
|
556 |
-
|
557 |
-
|
558 |
-
|
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 |
-
|
573 |
-
|
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:
|
591 |
}
|
592 |
-
|
593 |
.card img {
|
594 |
-
width:
|
595 |
-
|
596 |
-
|
597 |
-
|
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:
|
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 |
-
|
620 |
-
|
621 |
-
|
622 |
-
|
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,
|
640 |
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
641 |
}
|
642 |
-
|
643 |
-
/* ์บ์ ์ํ ๋ฑ์ง */
|
644 |
.cached-status {
|
645 |
-
position:
|
646 |
-
|
647 |
-
|
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 |
-
|
661 |
-
|
662 |
-
|
663 |
-
|
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:
|
681 |
-
|
682 |
-
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
|
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 |
-
|
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:
|
704 |
-
|
705 |
-
|
706 |
-
|
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:
|
721 |
}
|
722 |
-
|
723 |
-
/* ๋ก๋ฉ ์ ๋๋ฉ์ด์
*/
|
724 |
-
@keyframes spin {
|
725 |
-
0% { transform: rotate(0deg); }
|
726 |
-
100% { transform: rotate(360deg); }
|
727 |
-
}
|
728 |
-
|
729 |
.loading-spinner {
|
730 |
-
border:
|
731 |
-
border-top:
|
732 |
-
|
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:
|
741 |
-
|
742 |
-
|
743 |
-
|
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:
|
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 |
-
.
|
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:
|
788 |
-
|
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:
|
797 |
-
|
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:
|
832 |
-
|
833 |
-
|
834 |
-
|
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 |
-
|
847 |
-
.
|
848 |
-
|
849 |
-
|
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;"
|
|
|
|
|
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 |
-
|
|
|
923 |
['click','touchstart'].forEach(evt=>{
|
924 |
-
document.addEventListener(evt,function u(){
|
925 |
-
|
926 |
-
|
|
|
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 |
-
|
939 |
-
|
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 |
-
|
951 |
-
|
952 |
-
e.preventDefault();
|
953 |
-
e.stopPropagation();
|
954 |
pdfInput.click();
|
955 |
};
|
956 |
}
|
957 |
}
|
958 |
|
959 |
-
function addCard(i, thumb, title, isCached
|
960 |
const d = document.createElement('div');
|
961 |
d.className = 'card fade-in';
|
962 |
-
d.onclick = ()
|
963 |
-
|
964 |
-
const
|
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
|
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 |
-
|
992 |
-
let done = 0;
|
993 |
-
files.forEach((f,i)=> {
|
994 |
const r=new FileReader();
|
995 |
r.onload=x=>{
|
996 |
-
pages[i] = {src:
|
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
|
1009 |
-
if(!file) return;
|
1010 |
-
|
1011 |
showLoading("PDF ๋ก๋ฉ ์ค...");
|
1012 |
-
|
1013 |
-
|
1014 |
-
|
1015 |
-
|
1016 |
-
|
1017 |
-
|
1018 |
-
|
|
|
|
|
|
|
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(
|
1029 |
-
console.error("PDF ๋ก๋ฉ ์ค๋ฅ:",
|
1030 |
hideLoading();
|
1031 |
showError("PDF ๋ก๋ฉ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
|
1032 |
});
|
@@ -1034,527 +719,382 @@ HTML = """
|
|
1034 |
fr.readAsArrayBuffer(file);
|
1035 |
};
|
1036 |
|
1037 |
-
function save(pages,
|
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
|
1046 |
showLoading("๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ก๋ฉ ์ค...");
|
1047 |
}
|
1048 |
-
|
1049 |
-
|
1050 |
-
const
|
1051 |
-
|
1052 |
-
|
1053 |
-
|
1054 |
-
|
1055 |
-
serverProjects
|
1056 |
-
|
1057 |
-
if (serverProjects.length === 0) {
|
1058 |
hideLoading();
|
1059 |
-
$id('noProjects').style.display
|
1060 |
return;
|
1061 |
}
|
1062 |
-
|
1063 |
-
|
1064 |
-
|
1065 |
-
|
1066 |
-
const pdfName = project.name;
|
1067 |
const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
|
1068 |
-
|
1069 |
try {
|
1070 |
-
|
1071 |
-
const
|
1072 |
-
|
1073 |
-
|
1074 |
-
|
1075 |
-
|
1076 |
-
|
1077 |
-
|
1078 |
-
|
1079 |
-
|
1080 |
-
|
1081 |
-
|
|
|
1082 |
}
|
1083 |
-
} catch
|
1084 |
-
console.error(\`์ธ๋ค์ผ ๋ก๋ ์ค๋ฅ (\${
|
1085 |
}
|
1086 |
-
|
1087 |
return null;
|
1088 |
});
|
1089 |
-
|
1090 |
-
|
1091 |
-
|
1092 |
-
results.filter(result => result !== null).forEach(result => {
|
1093 |
-
save(result.pages, result.name, result.isCached);
|
1094 |
});
|
1095 |
-
|
1096 |
hideLoading();
|
1097 |
-
|
1098 |
-
|
1099 |
-
$id('noProjects').style.display = 'block';
|
1100 |
}
|
1101 |
-
} catch(
|
1102 |
-
console.error(
|
1103 |
hideLoading();
|
1104 |
showError("๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ก๋ฉ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
|
1105 |
}
|
1106 |
}
|
1107 |
|
1108 |
-
|
1109 |
-
async function checkCacheStatus() {
|
1110 |
try {
|
1111 |
-
const
|
1112 |
-
const cacheStatus
|
1113 |
-
|
1114 |
-
|
1115 |
-
|
1116 |
-
|
1117 |
-
|
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 |
-
|
1125 |
-
|
1126 |
-
badgeEl
|
1127 |
-
badgeEl.
|
1128 |
-
badgeEl.textContent = '์บ์๋จ';
|
1129 |
cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
|
1130 |
-
} else if
|
1131 |
-
badgeEl.textContent
|
1132 |
-
badgeEl.style.background
|
1133 |
}
|
1134 |
-
projects[i][0].cached
|
1135 |
-
} else if(cacheStatus[pdfName]
|
1136 |
-
if(!badgeEl)
|
1137 |
-
badgeEl
|
1138 |
-
badgeEl.className
|
1139 |
cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
|
1140 |
}
|
1141 |
-
badgeEl.textContent
|
1142 |
-
badgeEl.style.background
|
1143 |
}
|
1144 |
}
|
1145 |
}
|
1146 |
-
|
1147 |
-
|
1148 |
-
|
1149 |
-
|
1150 |
-
|
1151 |
-
|
1152 |
-
const progress = cacheStatus[pdfName].progress || 0;
|
1153 |
-
|
1154 |
-
if (status === "completed") {
|
1155 |
clearInterval(pageLoadingInterval);
|
1156 |
-
$id('loadingPages').style.display
|
1157 |
-
currentLoadingPdfPath
|
1158 |
refreshFlipBook();
|
1159 |
-
} else if
|
1160 |
-
$id('loadingPages').style.display
|
1161 |
-
$id('loadingPagesCount').textContent
|
1162 |
}
|
1163 |
}
|
1164 |
}
|
1165 |
-
} catch(
|
1166 |
-
console.error(
|
1167 |
}
|
1168 |
}
|
1169 |
|
1170 |
-
|
1171 |
-
async function open(i) {
|
1172 |
toggle(false);
|
1173 |
-
const pages
|
1174 |
-
|
1175 |
-
if(
|
1176 |
-
fb.destroy();
|
1177 |
-
viewer.innerHTML = '';
|
1178 |
-
}
|
1179 |
-
|
1180 |
-
if(pages[0].path) {
|
1181 |
const pdfPath = pages[0].path;
|
1182 |
-
let
|
1183 |
-
currentLoadingPdfPath
|
1184 |
-
|
1185 |
-
if(pages[0].cached) {
|
1186 |
showLoading("์บ์๋ PDF ๋ก๋ฉ ์ค...");
|
1187 |
try {
|
1188 |
-
const
|
1189 |
-
const
|
1190 |
-
|
1191 |
-
|
1192 |
-
|
1193 |
-
createFlipBook(
|
1194 |
-
|
1195 |
-
|
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 |
-
}
|
1203 |
-
console.error("์บ์ ๋ฐ์ดํฐ ๋ก๋ ์ค๋ฅ:", error);
|
1204 |
-
}
|
1205 |
}
|
1206 |
-
|
1207 |
-
if(!progressiveLoading) {
|
1208 |
showLoading("PDF ์ค๋น ์ค...");
|
1209 |
try {
|
1210 |
-
const
|
1211 |
-
const
|
1212 |
-
|
1213 |
-
|
1214 |
-
const
|
1215 |
-
|
1216 |
-
|
1217 |
-
if(
|
1218 |
-
hideLoading();
|
1219 |
-
|
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
|
1231 |
-
|
1232 |
-
try
|
1233 |
-
const
|
1234 |
-
if
|
1235 |
-
const
|
1236 |
-
const
|
1237 |
-
|
1238 |
-
|
1239 |
-
|
1240 |
-
|
1241 |
-
if(cachedData.status === "processing") {
|
1242 |
-
startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
|
1243 |
} else {
|
1244 |
-
currentLoadingPdfPath
|
1245 |
}
|
1246 |
return;
|
1247 |
}
|
1248 |
}
|
1249 |
-
}
|
1250 |
-
|
|
|
|
|
|
|
|
|
|
|
1251 |
}
|
1252 |
-
|
1253 |
-
const
|
1254 |
-
|
1255 |
-
const pdfPages = [];
|
1256 |
-
|
1257 |
-
for(let p=1; p <= pdf.numPages; p++) {
|
1258 |
updateLoading(\`ํ์ด์ง ์ค๋น ์ค... (\${p}/\${pdf.numPages})\`);
|
1259 |
-
const pg
|
1260 |
-
const vp
|
1261 |
-
const c
|
1262 |
-
c.width
|
1263 |
-
c.
|
1264 |
-
|
1265 |
-
pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
|
1266 |
}
|
1267 |
hideLoading();
|
1268 |
createFlipBook(pdfPages);
|
1269 |
-
currentLoadingPdfPath
|
1270 |
-
} catch(
|
1271 |
-
console.error(
|
1272 |
hideLoading();
|
1273 |
-
showError("PDF
|
1274 |
-
currentLoadingPdfPath
|
1275 |
}
|
1276 |
}
|
1277 |
} else {
|
1278 |
createFlipBook(pages);
|
1279 |
-
currentLoadingPdfPath
|
1280 |
}
|
1281 |
}
|
1282 |
|
1283 |
-
function startProgressiveLoadingIndicator(progress, totalPages)
|
1284 |
-
$id('loadingPages').style.display
|
1285 |
-
$id('loadingPagesCount').textContent
|
1286 |
-
|
1287 |
-
|
1288 |
-
|
1289 |
-
}
|
1290 |
-
pageLoadingInterval = setInterval(async () => {
|
1291 |
-
if (!currentLoadingPdfPath) {
|
1292 |
clearInterval(pageLoadingInterval);
|
1293 |
-
$id('loadingPages').style.display
|
1294 |
return;
|
1295 |
}
|
1296 |
try {
|
1297 |
-
const
|
1298 |
-
const
|
1299 |
-
|
1300 |
-
if (status.status === "completed") {
|
1301 |
clearInterval(pageLoadingInterval);
|
1302 |
-
$id('loadingPages').style.display
|
1303 |
refreshFlipBook();
|
1304 |
-
currentLoadingPdfPath
|
1305 |
-
} else if
|
1306 |
-
$id('loadingPagesCount').textContent
|
1307 |
}
|
1308 |
-
}
|
1309 |
-
console.error("์บ์ ์ํ ํ์ธ ์ค๋ฅ:", e);
|
1310 |
-
}
|
1311 |
}, 1000);
|
1312 |
}
|
1313 |
|
1314 |
-
async function refreshFlipBook()
|
1315 |
-
if
|
1316 |
-
try
|
1317 |
-
const
|
1318 |
-
const
|
1319 |
-
|
1320 |
-
|
1321 |
-
|
1322 |
-
|
1323 |
-
createFlipBook(cachedData.pages);
|
1324 |
-
currentLoadingPdfPath = null;
|
1325 |
}
|
1326 |
-
} catch
|
1327 |
-
console.error("ํ๋ฆฝ๋ถ ์๋ก๊ณ ์นจ ์ค๋ฅ:", e);
|
1328 |
-
}
|
1329 |
}
|
1330 |
|
1331 |
-
function createFlipBook(pages)
|
1332 |
-
|
1333 |
-
|
1334 |
-
|
1335 |
-
const
|
1336 |
-
|
1337 |
-
|
1338 |
-
|
1339 |
-
|
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 |
-
|
1349 |
-
|
1350 |
-
if
|
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 |
-
|
1362 |
-
viewer.style.
|
1363 |
-
|
1364 |
-
|
1365 |
-
|
1366 |
-
|
1367 |
-
return {
|
1368 |
-
src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+',
|
1369 |
-
thumb: page && page.thumb ? page.thumb : ''
|
1370 |
-
};
|
1371 |
}
|
1372 |
-
return
|
1373 |
});
|
1374 |
-
|
1375 |
-
fb = new FlipBook(viewer, {
|
1376 |
pages: validPages,
|
1377 |
-
viewMode:
|
1378 |
-
|
1379 |
-
|
1380 |
-
|
1381 |
-
|
1382 |
-
|
1383 |
-
|
1384 |
-
|
1385 |
-
|
1386 |
-
|
1387 |
-
|
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 |
-
|
1411 |
-
|
1412 |
-
|
1413 |
-
viewer.style.width = newSize.width + 'px';
|
1414 |
-
viewer.style.height = newSize.height + 'px';
|
1415 |
fb.resize();
|
1416 |
}
|
1417 |
});
|
1418 |
-
|
1419 |
-
|
1420 |
-
|
1421 |
-
|
1422 |
-
|
1423 |
-
|
1424 |
-
|
1425 |
-
|
1426 |
-
|
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
|
1450 |
-
|
1451 |
-
|
1452 |
-
}
|
1453 |
-
$id('loadingPages').style.display = 'none';
|
1454 |
-
currentLoadingPdfPath = null;
|
1455 |
};
|
1456 |
|
1457 |
function toggle(showHome){
|
1458 |
-
$id('home').style.display
|
1459 |
-
$id('viewerPage').style.display
|
1460 |
-
$id('homeButton').style.display
|
1461 |
-
|
1462 |
-
|
1463 |
-
document.body.classList.add('viewer-mode');
|
1464 |
-
} else {
|
1465 |
-
document.body.classList.remove('viewer-mode');
|
1466 |
-
}
|
1467 |
}
|
1468 |
|
1469 |
-
function showLoading(
|
1470 |
hideLoading();
|
1471 |
-
const
|
1472 |
-
|
1473 |
-
|
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
|
1480 |
</div>
|
1481 |
`;
|
1482 |
}
|
1483 |
-
|
1484 |
-
loadingContainer.innerHTML = `
|
1485 |
<div class="loading-spinner"></div>
|
1486 |
-
<p class="loading-text" id="loadingText">${
|
1487 |
-
${
|
1488 |
`;
|
1489 |
-
|
1490 |
-
document.body.appendChild(loadingContainer);
|
1491 |
}
|
1492 |
-
|
1493 |
-
|
1494 |
-
|
1495 |
-
if
|
1496 |
-
|
1497 |
-
|
1498 |
-
|
1499 |
-
|
1500 |
-
|
1501 |
-
|
1502 |
-
|
1503 |
-
|
1504 |
-
|
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 |
-
|
1512 |
}
|
1513 |
}
|
1514 |
}
|
1515 |
-
|
1516 |
-
|
1517 |
-
|
1518 |
-
if (loadingContainer) {
|
1519 |
-
loadingContainer.remove();
|
1520 |
-
}
|
1521 |
}
|
1522 |
-
|
1523 |
-
|
1524 |
-
const
|
1525 |
-
|
1526 |
-
|
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 |
-
|
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>
|