ginipick commited on
Commit
8cdeb51
ยท
1 Parent(s): 3e38fd8

Upload app (37).py

Browse files
Files changed (1) hide show
  1. app (37).py +1665 -0
app (37).py ADDED
@@ -0,0 +1,1665 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ display: flex;
454
+ justify-content: center;
455
+ align-items: center;
456
+ width: 100%;
457
+ height: 100%;
458
+ font-size: 22px;
459
+ color: var(--primary-color);
460
+ transition: var(--transition);
461
+ }
462
+
463
+ .floating-home:hover .icon {
464
+ color: #8bc5f8;
465
+ }
466
+
467
+ .floating-home .title {
468
+ position: absolute;
469
+ left: 70px;
470
+ background: rgba(255, 255, 255, 0.95);
471
+ padding: 8px 20px;
472
+ border-radius: 20px;
473
+ box-shadow: var(--shadow-sm);
474
+ font-weight: 600;
475
+ font-size: 14px;
476
+ white-space: nowrap;
477
+ pointer-events: none;
478
+ opacity: 0;
479
+ transform: translateX(-10px);
480
+ transition: all 0.3s ease;
481
+ }
482
+
483
+ .floating-home:hover .title {
484
+ opacity: 1;
485
+ transform: translateX(0);
486
+ }
487
+
488
+ #home, #viewerPage {
489
+ padding-top: 100px;
490
+ max-width: 1200px;
491
+ margin: 0 auto;
492
+ padding-bottom: 60px;
493
+ padding-left: 30px;
494
+ padding-right: 30px;
495
+ position: relative;
496
+ }
497
+
498
+ /* ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
499
+ .upload-container {
500
+ display: flex;
501
+ margin-bottom: 30px;
502
+ justify-content: center;
503
+ }
504
+
505
+ button.upload {
506
+ all: unset;
507
+ cursor: pointer;
508
+ padding: 12px 20px;
509
+ border-radius: var(--radius-md);
510
+ background: white;
511
+ margin: 0 10px;
512
+ font-weight: 500;
513
+ display: flex;
514
+ align-items: center;
515
+ box-shadow: var(--shadow-sm);
516
+ transition: var(--transition);
517
+ position: relative;
518
+ overflow: hidden;
519
+ }
520
+
521
+ button.upload::before {
522
+ content: '';
523
+ position: absolute;
524
+ top: 0;
525
+ left: 0;
526
+ width: 100%;
527
+ height: 100%;
528
+ background: linear-gradient(120deg, var(--primary-color) 0%, var(--secondary-color) 100%);
529
+ opacity: 0.08;
530
+ z-index: -1;
531
+ }
532
+
533
+ button.upload:hover {
534
+ transform: translateY(-3px);
535
+ box-shadow: var(--shadow-md);
536
+ }
537
+
538
+ button.upload:hover::before {
539
+ opacity: 0.15;
540
+ }
541
+
542
+ button.upload i {
543
+ margin-right: 8px;
544
+ font-size: 20px;
545
+ }
546
+
547
+ /* ๊ทธ๋ฆฌ๋“œ ๋ฐ ์นด๋“œ ์Šคํƒ€์ผ */
548
+ .grid {
549
+ display: grid;
550
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
551
+ gap: 24px;
552
+ margin-top: 36px;
553
+ }
554
+
555
+ .card {
556
+ background: var(--card-bg);
557
+ border-radius: var(--radius-md);
558
+ cursor: pointer;
559
+ box-shadow: var(--shadow-sm);
560
+ width: 100%;
561
+ height: 280px;
562
+ position: relative;
563
+ display: flex;
564
+ flex-direction: column;
565
+ align-items: center;
566
+ justify-content: center;
567
+ transition: var(--transition);
568
+ overflow: hidden;
569
+ }
570
+
571
+ .card::before {
572
+ content: '';
573
+ position: absolute;
574
+ top: 0;
575
+ left: 0;
576
+ width: 100%;
577
+ height: 100%;
578
+ background: linear-gradient(135deg, var(--secondary-color) 0%, var(--primary-color) 100%);
579
+ opacity: 0.06;
580
+ z-index: 1;
581
+ }
582
+
583
+ .card::after {
584
+ content: '';
585
+ position: absolute;
586
+ top: 0;
587
+ left: 0;
588
+ width: 100%;
589
+ height: 30%;
590
+ background: linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
591
+ z-index: 2;
592
+ }
593
+
594
+ .card img {
595
+ width: 65%;
596
+ height: auto;
597
+ object-fit: contain;
598
+ position: absolute;
599
+ top: 50%;
600
+ left: 50%;
601
+ transform: translate(-50%, -65%);
602
+ border: 1px solid rgba(0,0,0,0.05);
603
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
604
+ z-index: 3;
605
+ transition: var(--transition);
606
+ }
607
+
608
+ .card:hover {
609
+ transform: translateY(-5px);
610
+ box-shadow: var(--shadow-md);
611
+ }
612
+
613
+ .card:hover img {
614
+ transform: translate(-50%, -65%) scale(1.03);
615
+ box-shadow: 0 8px 20px rgba(0,0,0,0.12);
616
+ }
617
+
618
+ .card p {
619
+ position: absolute;
620
+ bottom: 20px;
621
+ left: 50%;
622
+ transform: translateX(-50%);
623
+ background: rgba(255, 255, 255, 0.9);
624
+ padding: 8px 16px;
625
+ border-radius: 30px;
626
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
627
+ width: 80%;
628
+ text-align: center;
629
+ white-space: nowrap;
630
+ overflow: hidden;
631
+ text-overflow: ellipsis;
632
+ font-size: 14px;
633
+ font-weight: 500;
634
+ color: var(--text-color);
635
+ z-index: 4;
636
+ transition: var(--transition);
637
+ }
638
+
639
+ .card:hover p {
640
+ background: rgba(255, 255, 255, 0.95);
641
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
642
+ }
643
+
644
+ /* ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ */
645
+ .cached-status {
646
+ position: absolute;
647
+ top: 10px;
648
+ right: 10px;
649
+ background: var(--accent-color);
650
+ color: white;
651
+ font-size: 11px;
652
+ padding: 3px 8px;
653
+ border-radius: 12px;
654
+ z-index: 5;
655
+ box-shadow: var(--shadow-sm);
656
+ }
657
+
658
+ /* ๋ทฐ์–ด ์Šคํƒ€์ผ */
659
+ #viewer {
660
+ width: 90%;
661
+ height: 90vh;
662
+ max-width: 90%;
663
+ margin: 0;
664
+ background: var(--card-bg);
665
+ border: none;
666
+ border-radius: var(--radius-lg);
667
+ position: fixed;
668
+ top: 50%;
669
+ left: 50%;
670
+ transform: translate(-50%, -50%);
671
+ z-index: 1000;
672
+ box-shadow: var(--shadow-lg);
673
+ max-height: calc(90vh - 40px);
674
+ aspect-ratio: auto;
675
+ object-fit: contain;
676
+ overflow: hidden;
677
+ }
678
+
679
+ /* FlipBook ์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ */
680
+ .flipbook-container .fb3d-menu-bar {
681
+ z-index: 2000 !important;
682
+ opacity: 1 !important;
683
+ bottom: 0 !important;
684
+ background-color: rgba(255,255,255,0.9) !important;
685
+ backdrop-filter: blur(10px) !important;
686
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg) !important;
687
+ padding: 12px 0 !important;
688
+ box-shadow: 0 -4px 20px rgba(0,0,0,0.1) !important;
689
+ }
690
+
691
+ .flipbook-container .fb3d-menu-bar > ul > li > img,
692
+ .flipbook-container .fb3d-menu-bar > ul > li > div {
693
+ opacity: 1 !important;
694
+ transform: scale(1.2) !important;
695
+ filter: drop-shadow(0 2px 3px rgba(0,0,0,0.1)) !important;
696
+ }
697
+
698
+ .flipbook-container .fb3d-menu-bar > ul > li {
699
+ margin: 0 12px !important;
700
+ }
701
+
702
+ /* ๋ฉ”๋‰ด ํˆดํŒ ์Šคํƒ€์ผ */
703
+ .flipbook-container .fb3d-menu-bar > ul > li > span {
704
+ background-color: rgba(0,0,0,0.7) !important;
705
+ color: white !important;
706
+ border-radius: var(--radius-sm) !important;
707
+ padding: 8px 12px !important;
708
+ font-size: 13px !important;
709
+ bottom: 55px !important;
710
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
711
+ letter-spacing: 0.3px !important;
712
+ }
713
+
714
+ /* ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ๋ฐฐ๊ฒฝ ์˜ค๋ฒ„๋ ˆ์ด */
715
+ .viewer-mode {
716
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
717
+ }
718
+
719
+ /* ๋ทฐ์–ด ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ */
720
+ #viewerPage {
721
+ background: transparent;
722
+ }
723
+
724
+ /* ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
725
+ @keyframes spin {
726
+ 0% { transform: rotate(0deg); }
727
+ 100% { transform: rotate(360deg); }
728
+ }
729
+
730
+ .loading-spinner {
731
+ border: 4px solid rgba(255,255,255,0.3);
732
+ border-top: 4px solid var(--primary-color);
733
+ border-radius: 50%;
734
+ width: 50px;
735
+ height: 50px;
736
+ margin: 0 auto;
737
+ animation: spin 1.5s ease-in-out infinite;
738
+ }
739
+
740
+ .loading-container {
741
+ position: absolute;
742
+ top: 50%;
743
+ left: 50%;
744
+ transform: translate(-50%, -50%);
745
+ text-align: center;
746
+ background: rgba(255, 255, 255, 0.85);
747
+ backdrop-filter: blur(10px);
748
+ padding: 30px;
749
+ border-radius: var(--radius-md);
750
+ box-shadow: var(--shadow-md);
751
+ z-index: 9999;
752
+ }
753
+
754
+ .loading-text {
755
+ margin-top: 20px;
756
+ font-size: 16px;
757
+ color: var(--text-color);
758
+ font-weight: 500;
759
+ }
760
+
761
+ /* ํŽ˜์ด์ง€ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
762
+ @keyframes fadeIn {
763
+ from { opacity: 0; }
764
+ to { opacity: 1; }
765
+ }
766
+
767
+ .fade-in {
768
+ animation: fadeIn 0.5s ease-out;
769
+ }
770
+
771
+ /* ์ถ”๊ฐ€ ์Šคํƒ€์ผ */
772
+ .section-title {
773
+ font-size: 1.3rem;
774
+ font-weight: 600;
775
+ margin: 30px 0 15px;
776
+ color: var(--text-color);
777
+ }
778
+
779
+ .no-projects {
780
+ text-align: center;
781
+ margin: 40px 0;
782
+ color: var(--text-color);
783
+ font-size: 16px;
784
+ }
785
+
786
+ /* ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” */
787
+ .progress-bar-container {
788
+ width: 100%;
789
+ height: 6px;
790
+ background-color: rgba(0,0,0,0.1);
791
+ border-radius: 3px;
792
+ margin-top: 15px;
793
+ overflow: hidden;
794
+ }
795
+
796
+ .progress-bar {
797
+ height: 100%;
798
+ background: linear-gradient(to right, var(--primary-color), var(--accent-color));
799
+ border-radius: 3px;
800
+ transition: width 0.3s ease;
801
+ }
802
+
803
+ /* ํ—ค๋” ๋กœ๊ณ  ๋ฐ ํƒ€์ดํ‹€ */
804
+ .library-header {
805
+ position: fixed;
806
+ top: 20px;
807
+ left: 0;
808
+ right: 0;
809
+ text-align: center;
810
+ z-index: 100;
811
+ pointer-events: none;
812
+ }
813
+
814
+ .library-header .title {
815
+ display: inline-block;
816
+ padding: 12px 30px;
817
+ background: rgba(255, 255, 255, 0.85);
818
+ backdrop-filter: blur(10px);
819
+ border-radius: 30px;
820
+ box-shadow: var(--shadow-md);
821
+ font-size: 1.5rem;
822
+ font-weight: 600;
823
+ background-image: linear-gradient(120deg, #667eea 0%, #764ba2 100%);
824
+ -webkit-background-clip: text;
825
+ background-clip: text;
826
+ color: transparent;
827
+ pointer-events: all;
828
+ }
829
+
830
+ /* ์ ์ง„์  ๋กœ๋”ฉ ํ‘œ์‹œ */
831
+ .loading-pages {
832
+ position: absolute;
833
+ bottom: 20px;
834
+ left: 50%;
835
+ transform: translateX(-50%);
836
+ background: rgba(255, 255, 255, 0.9);
837
+ padding: 10px 20px;
838
+ border-radius: 20px;
839
+ box-shadow: var(--shadow-md);
840
+ font-size: 14px;
841
+ color: var(--text-color);
842
+ z-index: 9998;
843
+ text-align: center;
844
+ }
845
+
846
+ /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
847
+ @media (max-width: 768px) {
848
+ .grid {
849
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
850
+ gap: 16px;
851
+ }
852
+
853
+ .card {
854
+ height: 240px;
855
+ }
856
+
857
+ .library-header .title {
858
+ font-size: 1.25rem;
859
+ padding: 10px 20px;
860
+ }
861
+
862
+ .floating-home {
863
+ width: 50px;
864
+ height: 50px;
865
+ }
866
+
867
+ .floating-home .icon {
868
+ font-size: 18px;
869
+ }
870
+ }
871
+ </style>
872
+ </head>
873
+ <body>
874
+ <!-- ์ œ๋ชฉ์„ Home ๋ฒ„ํŠผ๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์–ด๋กœ ์ฒ˜๋ฆฌ -->
875
+ <div id="homeButton" class="floating-home" style="display:none;">
876
+ <div class="icon"><i class="fas fa-home"></i></div>
877
+ <div class="title">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</div>
878
+ </div>
879
+
880
+ <!-- ์„ผํ„ฐ ์ •๋ ฌ๋œ ํƒ€์ดํ‹€ -->
881
+ <div class="library-header">
882
+ <div class="title">FlipBook Library</div>
883
+ </div>
884
+
885
+ <section id="home" class="fade-in">
886
+ <div class="upload-container">
887
+ <button class="upload" id="imageUploadBtn">
888
+ <i class="fas fa-images"></i> ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
889
+ <input id="imgInput" type="file" accept="image/*" multiple hidden>
890
+ </button>
891
+ <button class="upload" id="pdfUploadBtn">
892
+ <i class="fas fa-file-pdf"></i> PDF ์ถ”๊ฐ€
893
+ <input id="pdfInput" type="file" accept="application/pdf" hidden>
894
+ </button>
895
+ </div>
896
+
897
+ <div class="section-title">๋‚ด ํ”„๋กœ์ ํŠธ</div>
898
+ <div class="grid" id="grid">
899
+ <!-- ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
900
+ </div>
901
+ <div id="noProjects" class="no-projects" style="display: none;">
902
+ ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๋‚˜ PDF๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
903
+ </div>
904
+ </section>
905
+
906
+ <section id="viewerPage" style="display:none">
907
+ <div id="viewer"></div>
908
+ <div id="loadingPages" class="loading-pages" style="display:none;">ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0/0</span></div>
909
+ </section>
910
+
911
+ <script>
912
+ let projects=[], fb=null;
913
+ const grid=$id('grid'), viewer=$id('viewer');
914
+ pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
915
+
916
+ // ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
917
+ let serverProjects = [];
918
+
919
+ // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ
920
+ let currentLoadingPdfPath = null;
921
+ let pageLoadingInterval = null;
922
+
923
+ /* ๐Ÿ”Š ์˜ค๋””์˜ค unlock โ€“ ๋‚ด์žฅ Audio ์™€ ๊ฐ™์€ MP3 ๊ฒฝ๋กœ ์‚ฌ์šฉ */
924
+ ['click','touchstart'].forEach(evt=>{
925
+ document.addEventListener(evt,function u(){new Audio('static/turnPage2.mp3')
926
+ .play().then(a=>a.pause()).catch(()=>{});document.removeEventListener(evt,u,{capture:true});},
927
+ {once:true,capture:true});
928
+ });
929
+
930
+ // ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ ํ•จ์ˆ˜
931
+ function setupUploadButtons() {
932
+ // ๋ชจ๋“  ์—…๋กœ๋“œ ๋ฒ„ํŠผ์— ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
933
+ document.querySelectorAll('.upload').forEach(button => {
934
+ button.addEventListener('click', function(e) {
935
+ // ๋ฒ„ํŠผ ๋‚ด๋ถ€์˜ input ์š”์†Œ ์ฐพ๊ธฐ
936
+ const inputElement = this.querySelector('input[type="file"]');
937
+ if (inputElement) {
938
+ // input ์š”์†Œ ํด๋ฆญ (ํŒŒ์ผ ์„ ํƒ ๋‹ค์ด์–ผ๋กœ๊ทธ ์—ด๊ธฐ)
939
+ inputElement.click();
940
+ e.preventDefault(); // ๋ฒ„ํŠผ ๊ธฐ๋ณธ ๋™์ž‘ ๋ฐฉ์ง€
941
+ }
942
+ });
943
+ });
944
+ }
945
+
946
+ /* โ”€โ”€ ์œ ํ‹ธ โ”€โ”€ */
947
+ function $id(id){return document.getElementById(id)}
948
+
949
+ // ์ง์ ‘ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ • (๋” ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•)
950
+ function setupDirectEvents() {
951
+ // ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ
952
+ const imageBtn = $id('imageUploadBtn');
953
+ const imageInput = $id('imgInput');
954
+ if (imageBtn && imageInput) {
955
+ console.log('์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •');
956
+ imageBtn.onclick = function(e) {
957
+ e.preventDefault();
958
+ e.stopPropagation();
959
+ imageInput.click();
960
+ };
961
+ }
962
+
963
+ // PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ
964
+ const pdfBtn = $id('pdfUploadBtn');
965
+ const pdfInput = $id('pdfInput');
966
+ if (pdfBtn && pdfInput) {
967
+ console.log('PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •');
968
+ pdfBtn.onclick = function(e) {
969
+ e.preventDefault();
970
+ e.stopPropagation();
971
+ pdfInput.click();
972
+ };
973
+ }
974
+ }
975
+
976
+ function addCard(i, thumb, title, isCached = false) {
977
+ const d = document.createElement('div');
978
+ d.className = 'card fade-in';
979
+ d.onclick = () => open(i);
980
+
981
+ // ์ œ๋ชฉ ์ฒ˜๋ฆฌ
982
+ const displayTitle = title ?
983
+ (title.length > 15 ? title.substring(0, 15) + '...' : title) :
984
+ 'ํ”„๋กœ์ ํŠธ ' + (i+1);
985
+
986
+ // ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์ถ”๊ฐ€
987
+ const cachedBadge = isCached ?
988
+ '<div class="cached-status">์บ์‹œ๋จ</div>' : '';
989
+
990
+ d.innerHTML = `
991
+ <div class="card-inner">
992
+ ${cachedBadge}
993
+ <img src="${thumb}" alt="${displayTitle}" loading="lazy">
994
+ <p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
995
+ </div>
996
+ `;
997
+ grid.appendChild(d);
998
+
999
+ // ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์œผ๋ฉด 'ํ”„๋กœ์ ํŠธ ์—†์Œ' ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
1000
+ $id('noProjects').style.display = 'none';
1001
+ }
1002
+
1003
+ /* โ”€โ”€ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ โ”€โ”€ */
1004
+ $id('imgInput').onchange=e=>{
1005
+ const files=[...e.target.files]; if(!files.length) return;
1006
+
1007
+ // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
1008
+ showLoading("์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘...");
1009
+
1010
+ const pages=[],tot=files.length;let done=0;
1011
+ files.forEach((f,i)=>{const r=new FileReader();r.onload=x=>{pages[i]={src:x.target.result,thumb:x.target.result};
1012
+ if(++done===tot) {
1013
+ save(pages, '์ด๋ฏธ์ง€ ์ปฌ๋ ‰์…˜');
1014
+ hideLoading();
1015
+ }
1016
+ };r.readAsDataURL(f);});
1017
+ };
1018
+
1019
+ /* โ”€โ”€ PDF ์—…๋กœ๋“œ โ”€โ”€ */
1020
+ $id('pdfInput').onchange=e=>{
1021
+ const file=e.target.files[0]; if(!file) return;
1022
+
1023
+ // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
1024
+ showLoading("PDF ๋กœ๋”ฉ ์ค‘...");
1025
+
1026
+ const fr=new FileReader();
1027
+ fr.onload=v=>{
1028
+ pdfjsLib.getDocument({data:v.target.result}).promise.then(async pdf=>{
1029
+ const pages=[];
1030
+
1031
+ for(let p=1;p<=pdf.numPages;p++){
1032
+ // ๋กœ๋”ฉ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1033
+ updateLoading(`PDF ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... (${p}/${pdf.numPages})`);
1034
+
1035
+ const pg=await pdf.getPage(p), vp=pg.getViewport({scale:1});
1036
+ const c=document.createElement('canvas');c.width=vp.width;c.height=vp.height;
1037
+ await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
1038
+ pages.push({src:c.toDataURL(),thumb:c.toDataURL()});
1039
+ }
1040
+
1041
+ hideLoading();
1042
+ save(pages, file.name.replace('.pdf', ''));
1043
+ }).catch(error => {
1044
+ console.error("PDF ๋กœ๋”ฉ ์˜ค๋ฅ˜:", error);
1045
+ hideLoading();
1046
+ showError("PDF ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1047
+ });
1048
+ };fr.readAsArrayBuffer(file);
1049
+ };
1050
+
1051
+ /* โ”€โ”€ ํ”„๋กœ์ ํŠธ ์ €์žฅ โ”€โ”€ */
1052
+ function save(pages, title, isCached = false){
1053
+ const id=projects.push(pages)-1;
1054
+ addCard(id, pages[0].thumb, title, isCached);
1055
+ }
1056
+
1057
+ /* โ”€โ”€ ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ โ”€โ”€ */
1058
+ async function loadServerPDFs() {
1059
+ try {
1060
+ // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
1061
+ if (document.querySelectorAll('.card').length === 0) {
1062
+ showLoading("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘...");
1063
+ }
1064
+
1065
+ // ๋จผ์ € ์บ์‹œ ์ƒํƒœ ํ™•์ธ
1066
+ const cacheStatusRes = await fetch('/api/cache-status');
1067
+ const cacheStatus = await cacheStatusRes.json();
1068
+
1069
+ // PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
1070
+ const response = await fetch('/api/pdf-projects');
1071
+ serverProjects = await response.json();
1072
+
1073
+ if (serverProjects.length === 0) {
1074
+ hideLoading();
1075
+ $id('noProjects').style.display = 'block';
1076
+ return;
1077
+ }
1078
+
1079
+ // ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์ธ๋„ค์ผ ์ƒ์„ฑ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์ตœ์ ํ™”)
1080
+ const thumbnailPromises = serverProjects.map(async (project, index) => {
1081
+ updateLoading(`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (${index+1}/${serverProjects.length})`);
1082
+
1083
+ const pdfName = project.name;
1084
+ const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
1085
+
1086
+ try {
1087
+ // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
1088
+ const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`);
1089
+ const data = await response.json();
1090
+
1091
+ if(data.thumbnail) {
1092
+ const pages = [{
1093
+ src: data.thumbnail,
1094
+ thumb: data.thumbnail,
1095
+ path: project.path,
1096
+ cached: isCached
1097
+ }];
1098
+
1099
+ return {
1100
+ pages,
1101
+ name: project.name,
1102
+ isCached
1103
+ };
1104
+ }
1105
+ } catch (err) {
1106
+ console.error(`์ธ๋„ค์ผ ๋กœ๋“œ ์˜ค๋ฅ˜ (${project.name}):`, err);
1107
+ }
1108
+
1109
+ return null;
1110
+ });
1111
+
1112
+ // ๋ชจ๋“  ์ธ๋„ค์ผ ์š”์ฒญ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ
1113
+ const results = await Promise.all(thumbnailPromises);
1114
+
1115
+ // ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ ๊ฒฐ๊ณผ๋งŒ ํ‘œ์‹œ
1116
+ results.filter(result => result !== null).forEach(result => {
1117
+ save(result.pages, result.name, result.isCached);
1118
+ });
1119
+
1120
+ hideLoading();
1121
+
1122
+ // ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
1123
+ if (document.querySelectorAll('.card').length === 0) {
1124
+ $id('noProjects').style.display = 'block';
1125
+ }
1126
+ } catch(error) {
1127
+ console.error('์„œ๋ฒ„ PDF ๋กœ๋“œ ์‹คํŒจ:', error);
1128
+ hideLoading();
1129
+ showError("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1130
+ }
1131
+ }
1132
+
1133
+ /* โ”€โ”€ ์บ์‹œ ์ƒํƒœ ์ •๊ธฐ์ ์œผ๋กœ ํ™•์ธ โ”€โ”€ */
1134
+ async function checkCacheStatus() {
1135
+ try {
1136
+ const response = await fetch('/api/cache-status');
1137
+ const cacheStatus = await response.json();
1138
+
1139
+ // ํ˜„์žฌ ์นด๋“œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1140
+ const cards = document.querySelectorAll('.card');
1141
+
1142
+ for(let i = 0; i < cards.length; i++) {
1143
+ if(projects[i] && projects[i][0] && projects[i][0].path) {
1144
+ const pdfPath = projects[i][0].path;
1145
+ const pdfName = pdfPath.split('/').pop().replace('.pdf', '');
1146
+
1147
+ // ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์—…๋ฐ์ดํŠธ
1148
+ let badgeEl = cards[i].querySelector('.cached-status');
1149
+
1150
+ if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed") {
1151
+ if(!badgeEl) {
1152
+ badgeEl = document.createElement('div');
1153
+ badgeEl.className = 'cached-status';
1154
+ badgeEl.textContent = '์บ์‹œ๋จ';
1155
+ cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1156
+ } else if (badgeEl.textContent !== '์บ์‹œ๋จ') {
1157
+ badgeEl.textContent = '์บ์‹œ๋จ';
1158
+ badgeEl.style.background = 'var(--accent-color)';
1159
+ }
1160
+ projects[i][0].cached = true;
1161
+ } else if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "processing") {
1162
+ if(!badgeEl) {
1163
+ badgeEl = document.createElement('div');
1164
+ badgeEl.className = 'cached-status';
1165
+ cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1166
+ }
1167
+ badgeEl.textContent = `${cacheStatus[pdfName].progress}%`;
1168
+ badgeEl.style.background = 'var(--secondary-color)';
1169
+ }
1170
+ }
1171
+ }
1172
+
1173
+ // ํ˜„์žฌ ๋กœ๋”ฉ ์ค‘์ธ PDF๊ฐ€ ์žˆ์œผ๋ฉด ์ƒํƒœ ํ™•์ธ
1174
+ if (currentLoadingPdfPath && pageLoadingInterval) {
1175
+ const pdfName = currentLoadingPdfPath.split('/').pop().replace('.pdf', '');
1176
+
1177
+ if (cacheStatus[pdfName]) {
1178
+ const status = cacheStatus[pdfName].status;
1179
+ const progress = cacheStatus[pdfName].progress || 0;
1180
+
1181
+ if (status === "completed") {
1182
+ // ์บ์‹ฑ ์™„๋ฃŒ ์‹œ
1183
+ clearInterval(pageLoadingInterval);
1184
+ $id('loadingPages').style.display = 'none';
1185
+ currentLoadingPdfPath = null;
1186
+
1187
+ // ์™„๋ฃŒ๋œ ์บ์‹œ๋กœ ํ”Œ๋ฆฝ๋ถ ๋‹ค์‹œ ๋กœ๋“œ
1188
+ refreshFlipBook();
1189
+ } else if (status === "processing") {
1190
+ // ์ง„ํ–‰ ์ค‘์ผ ๋•Œ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
1191
+ $id('loadingPages').style.display = 'block';
1192
+ $id('loadingPagesCount').textContent = `${progress}%`;
1193
+ }
1194
+ }
1195
+ }
1196
+
1197
+ } catch(error) {
1198
+ console.error('์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
1199
+ }
1200
+ }
1201
+
1202
+ /* โ”€โ”€ ์นด๋“œ โ†’ FlipBook โ”€โ”€ */
1203
+ async function open(i) {
1204
+ toggle(false);
1205
+ const pages = projects[i];
1206
+
1207
+ // ๊ธฐ์กด FlipBook ์ •๋ฆฌ
1208
+ if(fb) {
1209
+ fb.destroy();
1210
+ viewer.innerHTML = '';
1211
+ }
1212
+
1213
+ // ์„œ๋ฒ„ PDF ๋˜๋Š” ๋กœ์ปฌ ํ”„๋กœ์ ํŠธ ์ฒ˜๋ฆฌ
1214
+ if(pages[0].path) {
1215
+ const pdfPath = pages[0].path;
1216
+
1217
+ // ์ ์ง„์  ๋กœ๋”ฉ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™”
1218
+ let progressiveLoading = false;
1219
+ currentLoadingPdfPath = pdfPath;
1220
+
1221
+ // ์บ์‹œ ์—ฌ๋ถ€ ํ™•์ธ
1222
+ if(pages[0].cached) {
1223
+ // ์บ์‹œ๋œ PDF ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
1224
+ showLoading("์บ์‹œ๋œ PDF ๋กœ๋”ฉ ์ค‘...");
1225
+
1226
+ try {
1227
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
1228
+ const cachedData = await response.json();
1229
+
1230
+ if(cachedData.status === "completed" && cachedData.pages) {
1231
+ hideLoading();
1232
+ createFlipBook(cachedData.pages);
1233
+ currentLoadingPdfPath = null;
1234
+ return;
1235
+ } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1236
+ // ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
1237
+ hideLoading();
1238
+ createFlipBook(cachedData.pages);
1239
+ progressiveLoading = true;
1240
+
1241
+ // ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
1242
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1243
+ }
1244
+ } catch(error) {
1245
+ console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
1246
+ // ์บ์‹œ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์›๋ณธ PDF๋กœ ๋Œ€์ฒด
1247
+ }
1248
+ }
1249
+
1250
+ if (!progressiveLoading) {
1251
+ // ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
1252
+ showLoading("PDF ์ค€๋น„ ์ค‘...");
1253
+
1254
+ try {
1255
+ const response = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1256
+ const data = await response.json();
1257
+
1258
+ // ์บ์‹œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋œ ๊ฒฝ์šฐ
1259
+ if(data.redirect) {
1260
+ const redirectRes = await fetch(data.redirect);
1261
+ const cachedData = await redirectRes.json();
1262
+
1263
+ if(cachedData.status === "completed" && cachedData.pages) {
1264
+ hideLoading();
1265
+ createFlipBook(cachedData.pages);
1266
+ currentLoadingPdfPath = null;
1267
+ return;
1268
+ } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1269
+ // ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
1270
+ hideLoading();
1271
+ createFlipBook(cachedData.pages);
1272
+
1273
+ // ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
1274
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1275
+ return;
1276
+ }
1277
+ }
1278
+
1279
+ // ์›๋ณธ PDF ๋กœ๋“œ (ArrayBuffer ํ˜•ํƒœ)
1280
+ const pdfResponse = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1281
+
1282
+ // JSON ์‘๋‹ต์ธ ๊ฒฝ์šฐ (๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋“ฑ)
1283
+ try {
1284
+ const jsonData = await pdfResponse.clone().json();
1285
+ if (jsonData.redirect) {
1286
+ const redirectRes = await fetch(jsonData.redirect);
1287
+ const cachedData = await redirectRes.json();
1288
+
1289
+ if(cachedData.pages && cachedData.pages.length > 0) {
1290
+ hideLoading();
1291
+ createFlipBook(cachedData.pages);
1292
+
1293
+ if(cachedData.status === "processing") {
1294
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1295
+ } else {
1296
+ currentLoadingPdfPath = null;
1297
+ }
1298
+ return;
1299
+ }
1300
+ }
1301
+ } catch (e) {
1302
+ // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์›๋ณธ PDF ๋ฐ์ดํ„ฐ๋กœ ์ฒ˜๋ฆฌ
1303
+ }
1304
+
1305
+ // ArrayBuffer ํ˜•ํƒœ์˜ PDF ๋ฐ์ดํ„ฐ
1306
+ const pdfData = await pdfResponse.arrayBuffer();
1307
+
1308
+ // PDF ๋กœ๋“œ ๋ฐ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
1309
+ const pdf = await pdfjsLib.getDocument({data: pdfData}).promise;
1310
+ const pdfPages = [];
1311
+
1312
+ for(let p = 1; p <= pdf.numPages; p++) {
1313
+ updateLoading(`ํŽ˜์ด์ง€ ์ค€๋น„ ์ค‘... (${p}/${pdf.numPages})`);
1314
+
1315
+ const pg = await pdf.getPage(p);
1316
+ const vp = pg.getViewport({scale: 1});
1317
+ const c = document.createElement('canvas');
1318
+ c.width = vp.width;
1319
+ c.height = vp.height;
1320
+
1321
+ await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
1322
+ pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
1323
+ }
1324
+
1325
+ hideLoading();
1326
+ createFlipBook(pdfPages);
1327
+ currentLoadingPdfPath = null;
1328
+
1329
+ } catch(error) {
1330
+ console.error('PDF ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
1331
+ hideLoading();
1332
+ showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1333
+ currentLoadingPdfPath = null;
1334
+ }
1335
+ }
1336
+ } else {
1337
+ // ๋กœ์ปฌ ์—…๋กœ๋“œ๋œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰
1338
+ createFlipBook(pages);
1339
+ currentLoadingPdfPath = null;
1340
+ }
1341
+ }
1342
+
1343
+ /* โ”€โ”€ ์ ์ง„์  ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์‹œ์ž‘ โ”€โ”€ */
1344
+ function startProgressiveLoadingIndicator(progress, totalPages) {
1345
+ // ์ง„ํ–‰ ์ƒํƒœ ํ‘œ์‹œ ํ™œ์„ฑํ™”
1346
+ $id('loadingPages').style.display = 'block';
1347
+ $id('loadingPagesCount').textContent = `${progress}%`;
1348
+
1349
+ // ๊ธฐ์กด ์ธํ„ฐ๋ฒŒ ์ œ๊ฑฐ
1350
+ if (pageLoadingInterval) {
1351
+ clearInterval(pageLoadingInterval);
1352
+ }
1353
+
1354
+ // ์ฃผ๊ธฐ์ ์œผ๋กœ ์บ์‹œ ์ƒํƒœ ํ™•์ธ (2์ดˆ๋งˆ๋‹ค)
1355
+ pageLoadingInterval = setInterval(async () => {
1356
+ if (!currentLoadingPdfPath) {
1357
+ clearInterval(pageLoadingInterval);
1358
+ $id('loadingPages').style.display = 'none';
1359
+ return;
1360
+ }
1361
+
1362
+ try {
1363
+ const response = await fetch(`/api/cache-status?path=${encodeURIComponent(currentLoadingPdfPath)}`);
1364
+ const status = await response.json();
1365
+
1366
+ // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
1367
+ if (status.status === "completed") {
1368
+ clearInterval(pageLoadingInterval);
1369
+ $id('loadingPages').style.display = 'none';
1370
+ refreshFlipBook(); // ์™„๋ฃŒ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ๊ณ ์นจ
1371
+ currentLoadingPdfPath = null;
1372
+ } else if (status.status === "processing") {
1373
+ $id('loadingPagesCount').textContent = `${status.progress}%`;
1374
+ }
1375
+ } catch (e) {
1376
+ console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
1377
+ }
1378
+ }, 1000);
1379
+ }
1380
+
1381
+ /* โ”€โ”€ ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ โ”€โ”€ */
1382
+ async function refreshFlipBook() {
1383
+ if (!currentLoadingPdfPath || !fb) return;
1384
+
1385
+ try {
1386
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(currentLoadingPdfPath)}`);
1387
+ const cachedData = await response.json();
1388
+
1389
+ if(cachedData.status === "completed" && cachedData.pages) {
1390
+ // ๊ธฐ์กด ํ”Œ๋ฆฝ๋ถ ์ •๋ฆฌ
1391
+ fb.destroy();
1392
+ viewer.innerHTML = '';
1393
+
1394
+ // ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ์žฌ์ƒ์„ฑ
1395
+ createFlipBook(cachedData.pages);
1396
+ currentLoadingPdfPath = null;
1397
+ }
1398
+ } catch (e) {
1399
+ console.error("ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜:", e);
1400
+ }
1401
+ }
1402
+
1403
+ function createFlipBook(pages) {
1404
+ console.log('FlipBook ์ƒ์„ฑ ์‹œ์ž‘. ํŽ˜์ด์ง€ ์ˆ˜:', pages.length);
1405
+
1406
+ try {
1407
+ // ํ™”๋ฉด ๋น„์œจ ๊ณ„์‚ฐ
1408
+ const calculateAspectRatio = () => {
1409
+ const windowWidth = window.innerWidth;
1410
+ const windowHeight = window.innerHeight;
1411
+ const aspectRatio = windowWidth / windowHeight;
1412
+
1413
+ // ๋„ˆ๋น„ ๋˜๋Š” ๋†’์ด ๊ธฐ์ค€์œผ๋กœ ์ตœ๋Œ€ 90% ์ œํ•œ
1414
+ let width, height;
1415
+ if (aspectRatio > 1) { // ๊ฐ€๋กœ ํ™”๋ฉด
1416
+ height = Math.min(windowHeight * 0.9, windowHeight - 40);
1417
+ width = height * aspectRatio * 0.8; // ๊ฐ€๋กœ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ๊ฐ„ ์ค„์ž„
1418
+ if (width > windowWidth * 0.9) {
1419
+ width = windowWidth * 0.9;
1420
+ height = width / (aspectRatio * 0.8);
1421
+ }
1422
+ } else { // ์„ธ๋กœ ํ™”๋ฉด
1423
+ width = Math.min(windowWidth * 0.9, windowWidth - 40);
1424
+ height = width / aspectRatio * 0.9; // ์„ธ๋กœ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ๊ฐ„ ๋Š˜๋ฆผ
1425
+ if (height > windowHeight * 0.9) {
1426
+ height = windowHeight * 0.9;
1427
+ width = height * aspectRatio * 0.9;
1428
+ }
1429
+ }
1430
+
1431
+ // ์ตœ์  ์‚ฌ์ด์ฆˆ ๋ฐ˜ํ™˜
1432
+ return {
1433
+ width: Math.round(width),
1434
+ height: Math.round(height)
1435
+ };
1436
+ };
1437
+
1438
+ // ์ดˆ๊ธฐ ํ™”๋ฉด ๋น„์œจ ๊ณ„์‚ฐ
1439
+ const size = calculateAspectRatio();
1440
+ viewer.style.width = size.width + 'px';
1441
+ viewer.style.height = size.height + 'px';
1442
+
1443
+ // ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ •์ œ (๋นˆ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ)
1444
+ const validPages = pages.map(page => {
1445
+ // src๊ฐ€ ์—†๋Š” ํŽ˜์ด์ง€๋Š” ๋กœ๋”ฉ ์ค‘ ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด
1446
+ if (!page || !page.src) {
1447
+ return {
1448
+ src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+',
1449
+ thumb: page && page.thumb ? page.thumb : ''
1450
+ };
1451
+ }
1452
+ return page;
1453
+ });
1454
+
1455
+ fb = new FlipBook(viewer, {
1456
+ pages: validPages,
1457
+ viewMode: 'webgl',
1458
+ autoSize: true,
1459
+ flipDuration: 800,
1460
+ backgroundColor: '#fff',
1461
+ /* ๐Ÿ”Š ๋‚ด์žฅ ์‚ฌ์šด๋“œ */
1462
+ sound: true,
1463
+ assets: {flipMp3: 'static/turnPage2.mp3', hardFlipMp3: 'static/turnPage2.mp3'},
1464
+ controlsProps: {
1465
+ enableFullscreen: true,
1466
+ enableToc: true,
1467
+ enableDownload: false,
1468
+ enablePrint: false,
1469
+ enableZoom: true,
1470
+ enableShare: false,
1471
+ enableSearch: true,
1472
+ enableAutoPlay: true,
1473
+ enableAnnotation: false,
1474
+ enableSound: true,
1475
+ enableLightbox: false,
1476
+ layout: 10, // ๋ ˆ์ด์•„์›ƒ ์˜ต์…˜
1477
+ skin: 'light', // ์Šคํ‚จ ์Šคํƒ€์ผ
1478
+ autoNavigationTime: 3600, // ์ž๋™ ๋„˜๊น€ ์‹œ๊ฐ„(์ดˆ)
1479
+ hideControls: false, // ์ปจํŠธ๋กค ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
1480
+ paddingTop: 10, // ์ƒ๋‹จ ํŒจ๋”ฉ
1481
+ paddingLeft: 10, // ์ขŒ์ธก ํŒจ๋”ฉ
1482
+ paddingRight: 10, // ์šฐ์ธก ํŒจ๋”ฉ
1483
+ paddingBottom: 10, // ํ•˜๋‹จ ํŒจ๋”ฉ
1484
+ pageTextureSize: 1024, // ํŽ˜์ด์ง€ ํ…์Šค์ฒ˜ ํฌ๊ธฐ
1485
+ thumbnails: true, // ์„ฌ๋„ค์ผ ํ™œ์„ฑํ™”
1486
+ autoHideControls: false, // ์ž๋™ ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
1487
+ controlsTimeout: 8000 // ์ปจํŠธ๋กค ํ‘œ์‹œ ์‹œ๊ฐ„ ์—ฐ์žฅ
1488
+ }
1489
+ });
1490
+
1491
+ // ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ FlipBook ํฌ๊ธฐ ์กฐ์ •
1492
+ window.addEventListener('resize', () => {
1493
+ if (fb) {
1494
+ const newSize = calculateAspectRatio();
1495
+ viewer.style.width = newSize.width + 'px';
1496
+ viewer.style.height = newSize.height + 'px';
1497
+ fb.resize();
1498
+ }
1499
+ });
1500
+
1501
+ // FlipBook ์ƒ์„ฑ ํ›„ ์ปจํŠธ๋กค๋ฐ” ๊ฐ•์ œ ํ‘œ์‹œ
1502
+ setTimeout(() => {
1503
+ try {
1504
+ // ์ปจํŠธ๋กค๋ฐ” ๊ด€๋ จ ์š”์†Œ ์ฐพ๊ธฐ ๋ฐ ์Šคํƒ€์ผ ์ ์šฉ
1505
+ const menuBars = document.querySelectorAll('.flipbook-container .fb3d-menu-bar');
1506
+ if (menuBars && menuBars.length > 0) {
1507
+ menuBars.forEach(menuBar => {
1508
+ menuBar.style.display = 'block';
1509
+ menuBar.style.opacity = '1';
1510
+ menuBar.style.visibility = 'visible';
1511
+ menuBar.style.zIndex = '9999';
1512
+ });
1513
+ }
1514
+ } catch (e) {
1515
+ console.warn('์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜:', e);
1516
+ }
1517
+ }, 1000);
1518
+
1519
+ console.log('FlipBook ์ƒ์„ฑ ์™„๋ฃŒ');
1520
+ } catch (error) {
1521
+ console.error('FlipBook ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
1522
+ showError("FlipBook์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1523
+ }
1524
+ }
1525
+
1526
+ /* โ”€โ”€ ๋„ค๋น„๊ฒŒ์ด์…˜ โ”€โ”€ */
1527
+ $id('homeButton').onclick=()=>{
1528
+ if(fb) {
1529
+ fb.destroy();
1530
+ viewer.innerHTML = '';
1531
+ fb = null;
1532
+ }
1533
+ toggle(true);
1534
+
1535
+ // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ •๋ฆฌ
1536
+ if (pageLoadingInterval) {
1537
+ clearInterval(pageLoadingInterval);
1538
+ pageLoadingInterval = null;
1539
+ }
1540
+ $id('loadingPages').style.display = 'none';
1541
+ currentLoadingPdfPath = null;
1542
+ };
1543
+
1544
+ function toggle(showHome){
1545
+ $id('home').style.display=showHome?'block':'none';
1546
+ $id('viewerPage').style.display=showHome?'none':'block';
1547
+ $id('homeButton').style.display=showHome?'none':'block';
1548
+
1549
+ // ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
1550
+ if(!showHome) {
1551
+ document.body.classList.add('viewer-mode');
1552
+ } else {
1553
+ document.body.classList.remove('viewer-mode');
1554
+ }
1555
+ }
1556
+
1557
+ /* -- ๋กœ๋”ฉ ๋ฐ ์˜ค๋ฅ˜ ํ‘œ์‹œ -- */
1558
+ function showLoading(message, progress = -1) {
1559
+ // ๊ธฐ์กด ๋กœ๋”ฉ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
1560
+ hideLoading();
1561
+
1562
+ const loadingContainer = document.createElement('div');
1563
+ loadingContainer.className = 'loading-container fade-in';
1564
+ loadingContainer.id = 'loadingContainer';
1565
+
1566
+ let progressBarHtml = '';
1567
+ if (progress >= 0) {
1568
+ progressBarHtml = `
1569
+ <div class="progress-bar-container">
1570
+ <div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>
1571
+ </div>
1572
+ `;
1573
+ }
1574
+
1575
+ loadingContainer.innerHTML = `
1576
+ <div class="loading-spinner"></div>
1577
+ <p class="loading-text" id="loadingText">${message || '๋กœ๋”ฉ ์ค‘...'}</p>
1578
+ ${progressBarHtml}
1579
+ `;
1580
+
1581
+ document.body.appendChild(loadingContainer);
1582
+ }
1583
+
1584
+ function updateLoading(message, progress = -1) {
1585
+ const loadingText = $id('loadingText');
1586
+ if (loadingText) {
1587
+ loadingText.textContent = message;
1588
+ }
1589
+
1590
+ if (progress >= 0) {
1591
+ let progressBar = $id('progressBar');
1592
+
1593
+ if (!progressBar) {
1594
+ const loadingContainer = $id('loadingContainer');
1595
+ if (loadingContainer) {
1596
+ const progressContainer = document.createElement('div');
1597
+ progressContainer.className = 'progress-bar-container';
1598
+ progressContainer.innerHTML = `<div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>`;
1599
+ loadingContainer.appendChild(progressContainer);
1600
+ progressBar = $id('progressBar');
1601
+ }
1602
+ } else {
1603
+ progressBar.style.width = `${progress}%`;
1604
+ }
1605
+ }
1606
+ }
1607
+
1608
+ function hideLoading() {
1609
+ const loadingContainer = $id('loadingContainer');
1610
+ if (loadingContainer) {
1611
+ loadingContainer.remove();
1612
+ }
1613
+ }
1614
+
1615
+ function showError(message) {
1616
+ // ๊ธฐ์กด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
1617
+ const existingError = $id('errorContainer');
1618
+ if (existingError) {
1619
+ existingError.remove();
1620
+ }
1621
+
1622
+ const errorContainer = document.createElement('div');
1623
+ errorContainer.className = 'loading-container fade-in';
1624
+ errorContainer.id = 'errorContainer';
1625
+ errorContainer.innerHTML = `
1626
+ <p class="loading-text" style="color: #e74c3c;">${message}</p>
1627
+ <button id="errorCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">ํ™•์ธ</button>
1628
+ `;
1629
+
1630
+ document.body.appendChild(errorContainer);
1631
+
1632
+ // ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
1633
+ $id('errorCloseBtn').onclick = () => {
1634
+ errorContainer.remove();
1635
+ };
1636
+
1637
+ // 5์ดˆ ํ›„ ์ž๋™์œผ๋กœ ๋‹ซ๊ธฐ
1638
+ setTimeout(() => {
1639
+ if ($id('errorContainer')) {
1640
+ $id('errorContainer').remove();
1641
+ }
1642
+ }, 5000);
1643
+ }
1644
+
1645
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
1646
+ window.addEventListener('DOMContentLoaded', () => {
1647
+ // ์ง์ ‘ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ • (๋” ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•)
1648
+ setupDirectEvents();
1649
+
1650
+ loadServerPDFs();
1651
+
1652
+ // ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ (3์ดˆ๋งˆ๋‹ค)
1653
+ setInterval(checkCacheStatus, 3000);
1654
+ });
1655
+ </script>
1656
+ </body>
1657
+ </html>
1658
+ """
1659
+
1660
+ @app.get("/", response_class=HTMLResponse)
1661
+ async def root():
1662
+ return get_html_content()
1663
+
1664
+ if __name__ == "__main__":
1665
+ uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))