ginipick commited on
Commit
675828a
ยท
verified ยท
1 Parent(s): a124d87

Create app.py

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