ginipick commited on
Commit
46b6b20
ยท
verified ยท
1 Parent(s): cd5ec42

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +572 -0
app.py ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
8
+ app.mount("/static", StaticFiles(directory=BASE), name="static")
9
+
10
+ # PDF ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
11
+ 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 = []
18
+ if PDF_DIR.exists():
19
+ pdf_files = [f for f in PDF_DIR.glob("*.pdf")]
20
+ return pdf_files
21
+
22
+ # PDF ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
23
+ def generate_pdf_projects():
24
+ projects_data = []
25
+ pdf_files = get_pdf_files()
26
+
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)))