ginipick commited on
Commit
d4f44df
ยท
verified ยท
1 Parent(s): 92a9b6b

Update app.py

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