ginipick commited on
Commit
57adc76
·
verified ·
1 Parent(s): d253c5d

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -572
app.py DELETED
@@ -1,572 +0,0 @@
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:95%;
104
- height:95vh;
105
- max-width:95%;
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(95vh - 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
- // 너비 또는 높이 기준으로 최대 95% 제한
354
- let width, height;
355
- if (aspectRatio > 1) { // 가로 화면
356
- height = Math.min(windowHeight * 0.95, windowHeight - 40);
357
- width = height * aspectRatio * 0.8; // 가로 화면에서는 약간 줄임
358
- if (width > windowWidth * 0.95) {
359
- width = windowWidth * 0.95;
360
- height = width / (aspectRatio * 0.8);
361
- }
362
- } else { // 세로 화면
363
- width = Math.min(windowWidth * 0.95, windowWidth - 40);
364
- height = width / aspectRatio * 0.9; // 세로 화면에서는 약간 늘림
365
- if (height > windowHeight * 0.95) {
366
- height = windowHeight * 0.95;
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)))