AI-BOOK / app.py
ginipick's picture
Update app.py
d4f44df verified
raw
history blame
44.4 kB
from fastapi import FastAPI, BackgroundTasks
from fastapi.responses import HTMLResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
import pathlib, os, uvicorn, base64, json
from typing import Dict, List, Any
import asyncio
import logging
# ๋กœ๊น… ์„ค์ •
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
BASE = pathlib.Path(__file__).parent
app = FastAPI()
app.mount("/static", StaticFiles(directory=BASE), name="static")
# PDF ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
PDF_DIR = BASE / "pdf"
if not PDF_DIR.exists():
PDF_DIR.mkdir(parents=True)
# ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
CACHE_DIR = BASE / "cache"
if not CACHE_DIR.exists():
CACHE_DIR.mkdir(parents=True)
# ์ „์—ญ ์บ์‹œ ๊ฐ์ฒด
pdf_cache: Dict[str, Dict[str, Any]] = {}
# PDF ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
def get_pdf_files():
pdf_files = []
if PDF_DIR.exists():
pdf_files = [f for f in PDF_DIR.glob("*.pdf")]
return pdf_files
# PDF ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
def generate_pdf_projects():
projects_data = []
pdf_files = get_pdf_files()
for pdf_file in pdf_files:
projects_data.append({
"path": str(pdf_file),
"name": pdf_file.stem,
"cached": pdf_file.stem in pdf_cache
})
return projects_data
# ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ ์ƒ์„ฑ
def get_cache_path(pdf_name: str):
return CACHE_DIR / f"{pdf_name}_cache.json"
# PDF ํŽ˜์ด์ง€ ์บ์‹ฑ ํ•จ์ˆ˜
async def cache_pdf(pdf_path: str):
try:
import fitz # PyMuPDF
pdf_file = pathlib.Path(pdf_path)
pdf_name = pdf_file.stem
# ์ด๋ฏธ ์บ์‹ฑ ์ค‘์ด๊ฑฐ๋‚˜ ์บ์‹ฑ ์™„๋ฃŒ๋œ PDF๋Š” ๊ฑด๋„ˆ๋›ฐ๊ธฐ
if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
logger.info(f"PDF {pdf_name} ์ด๋ฏธ ์บ์‹ฑ ์™„๋ฃŒ ๋˜๋Š” ์ง„ํ–‰ ์ค‘")
return
# ์บ์‹œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
pdf_cache[pdf_name] = {"status": "processing", "progress": 0, "pages": []}
# ์บ์‹œ ํŒŒ์ผ์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
cache_path = get_cache_path(pdf_name)
if cache_path.exists():
try:
with open(cache_path, "r") as cache_file:
cached_data = json.load(cache_file)
pdf_cache[pdf_name] = cached_data
pdf_cache[pdf_name]["status"] = "completed"
logger.info(f"์บ์‹œ ํŒŒ์ผ์—์„œ {pdf_name} ๋กœ๋“œ ์™„๋ฃŒ")
return
except Exception as e:
logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์‹คํŒจ: {e}")
# ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ ์ƒˆ๋กœ ์บ์‹ฑ
# PDF ํŒŒ์ผ ์—ด๊ธฐ
doc = fitz.open(pdf_path)
total_pages = doc.page_count
# ๊ฐ ํŽ˜์ด์ง€๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ์บ์‹ฑ
pages = []
for page_num in range(total_pages):
# ์ง„ํ–‰ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
progress = round((page_num + 1) / total_pages * 100)
pdf_cache[pdf_name]["progress"] = progress
# ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
page = doc[page_num]
pix = page.get_pixmap() # ๊ธฐ๋ณธ ํ•ด์ƒ๋„๋กœ ๋ Œ๋”๋ง
img_data = pix.tobytes("png")
# Base64 ์ธ์ฝ”๋”ฉ
b64_img = base64.b64encode(img_data).decode('utf-8')
img_src = f"data:image/png;base64,{b64_img}"
# ์ธ๋„ค์ผ์šฉ ์ž‘์€ ์ด๋ฏธ์ง€ (์ฒซ ํŽ˜์ด์ง€๋งŒ)
if page_num == 0:
pix_thumb = page.get_pixmap(matrix=fitz.Matrix(0.3, 0.3)) # ๋” ์ž‘์€ ์ธ๋„ค์ผ
thumb_data = pix_thumb.tobytes("png")
b64_thumb = base64.b64encode(thumb_data).decode('utf-8')
thumb_src = f"data:image/png;base64,{b64_thumb}"
else:
thumb_src = "" # ์ฒซ ํŽ˜์ด์ง€ ์™ธ์—๋Š” ์ธ๋„ค์ผ ์ƒ๋žต
# ํŽ˜์ด์ง€ ์ •๋ณด ์ €์žฅ
pages.append({
"src": img_src,
"thumb": thumb_src if page_num == 0 else ""
})
# ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์ค‘๊ฐ„ ์ €์žฅ (20ํŽ˜์ด์ง€๋งˆ๋‹ค)
if (page_num + 1) % 20 == 0 or page_num == total_pages - 1:
pdf_cache[pdf_name]["pages"] = pages
# ์บ์‹œ ํŒŒ์ผ ์ €์žฅ (์ง„ํ–‰ ์ค‘)
try:
with open(cache_path, "w") as cache_file:
json.dump({
"status": "processing",
"progress": progress,
"pages": pages,
"total_pages": total_pages
}, cache_file)
except Exception as e:
logger.error(f"์ค‘๊ฐ„ ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
# ์บ์‹ฑ ์™„๋ฃŒ
pdf_cache[pdf_name] = {
"status": "completed",
"progress": 100,
"pages": pages,
"total_pages": total_pages
}
# ์ตœ์ข… ์บ์‹œ ํŒŒ์ผ ์ €์žฅ
try:
with open(cache_path, "w") as cache_file:
json.dump(pdf_cache[pdf_name], cache_file)
logger.info(f"PDF {pdf_name} ์บ์‹ฑ ์™„๋ฃŒ, {total_pages}ํŽ˜์ด์ง€")
except Exception as e:
logger.error(f"์ตœ์ข… ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
except Exception as e:
import traceback
logger.error(f"PDF ์บ์‹ฑ ์˜ค๋ฅ˜: {str(e)}\n{traceback.format_exc()}")
if pdf_name in pdf_cache:
pdf_cache[pdf_name]["status"] = "error"
pdf_cache[pdf_name]["error"] = str(e)
# ์‹œ์ž‘ ์‹œ ๋ชจ๋“  PDF ํŒŒ์ผ ์บ์‹ฑ
async def init_cache_all_pdfs():
logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
pdf_files = get_pdf_files()
# ์ด๋ฏธ ์บ์‹œ๋œ PDF ํŒŒ์ผ ๋กœ๋“œ
for cache_file in CACHE_DIR.glob("*_cache.json"):
try:
pdf_name = cache_file.stem.replace("_cache", "")
with open(cache_file, "r") as f:
pdf_cache[pdf_name] = json.load(f)
pdf_cache[pdf_name]["status"] = "completed"
logger.info(f"๊ธฐ์กด ์บ์‹œ ๋กœ๋“œ: {pdf_name}")
except Exception as e:
logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}")
# ์บ์‹ฑ๋˜์ง€ ์•Š์€ PDF ํŒŒ์ผ ์ฒ˜๋ฆฌ
for pdf_file in pdf_files:
pdf_name = pdf_file.stem
if pdf_name not in pdf_cache or pdf_cache[pdf_name].get("status") != "completed":
try:
await cache_pdf(str(pdf_file))
except Exception as e:
logger.error(f"PDF {pdf_name} ์บ์‹ฑ ์˜ค๋ฅ˜: {str(e)}")
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹œ์ž‘ ํ•จ์ˆ˜
@app.on_event("startup")
async def startup_event():
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒœ์Šคํฌ๋กœ ์บ์‹ฑ ์‹คํ–‰
asyncio.create_task(init_cache_all_pdfs())
# API ์—”๋“œํฌ์ธํŠธ: PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก
@app.get("/api/pdf-projects")
async def get_pdf_projects_api():
return generate_pdf_projects()
# API ์—”๋“œํฌ์ธํŠธ: PDF ์ธ๋„ค์ผ ์ œ๊ณต
@app.get("/api/pdf-thumbnail")
async def get_pdf_thumbnail(path: str):
try:
pdf_file = pathlib.Path(path)
pdf_name = pdf_file.stem
# ์บ์‹œ์—์„œ ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed":
if pdf_cache[pdf_name]["pages"] and pdf_cache[pdf_name]["pages"][0].get("thumb"):
return {"thumbnail": pdf_cache[pdf_name]["pages"][0]["thumb"]}
# ์บ์‹œ์— ์—†์œผ๋ฉด ์ƒ์„ฑ
import fitz
doc = fitz.open(path)
if doc.page_count > 0:
page = doc[0]
pix = page.get_pixmap(matrix=fitz.Matrix(0.5, 0.5))
img_data = pix.tobytes("png")
b64_img = base64.b64encode(img_data).decode('utf-8')
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
asyncio.create_task(cache_pdf(path))
return {"thumbnail": f"data:image/png;base64,{b64_img}"}
return {"thumbnail": None}
except Exception as e:
logger.error(f"์ธ๋„ค์ผ ์ƒ์„ฑ ์˜ค๋ฅ˜: {str(e)}")
return {"error": str(e), "thumbnail": None}
# API ์—”๋“œํฌ์ธํŠธ: ์บ์‹œ ์ƒํƒœ ํ™•์ธ
@app.get("/api/cache-status")
async def get_cache_status(path: str = None):
if path:
pdf_file = pathlib.Path(path)
pdf_name = pdf_file.stem
if pdf_name in pdf_cache:
return pdf_cache[pdf_name]
return {"status": "not_cached"}
else:
return {name: {"status": info["status"], "progress": info.get("progress", 0)}
for name, info in pdf_cache.items()}
# API ์—”๋“œํฌ์ธํŠธ: ์บ์‹œ๋œ PDF ์ฝ˜ํ…์ธ  ์ œ๊ณต
@app.get("/api/cached-pdf")
async def get_cached_pdf(path: str, background_tasks: BackgroundTasks):
try:
pdf_file = pathlib.Path(path)
pdf_name = pdf_file.stem
# ์บ์‹œ ํ™•์ธ
if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed":
return pdf_cache[pdf_name]
# ์บ์‹œ๊ฐ€ ์ง„ํ–‰์ค‘์ธ ๊ฒฝ์šฐ
if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "processing":
return {"status": "processing", "progress": pdf_cache[pdf_name].get("progress", 0)}
# ์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
background_tasks.add_task(cache_pdf, path)
return {"status": "started", "progress": 0}
except Exception as e:
logger.error(f"์บ์‹œ๋œ PDF ์ œ๊ณต ์˜ค๋ฅ˜: {str(e)}")
return {"error": str(e), "status": "error"}
# API ์—”๋“œํฌ์ธํŠธ: PDF ์›๋ณธ ์ฝ˜ํ…์ธ  ์ œ๊ณต(์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)
@app.get("/api/pdf-content")
async def get_pdf_content(path: str, background_tasks: BackgroundTasks):
try:
# ์บ์‹ฑ ์ƒํƒœ ํ™•์ธ
pdf_file = pathlib.Path(path)
if not pdf_file.exists():
return JSONResponse(content={"error": f"ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {path}"}, status_code=404)
pdf_name = pdf_file.stem
# ์บ์‹œ๋œ ๊ฒฝ์šฐ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed":
return JSONResponse(content={"redirect": f"/api/cached-pdf?path={path}"})
# ํŒŒ์ผ ์ฝ๊ธฐ
with open(path, "rb") as pdf_file:
content = pdf_file.read()
# ํŒŒ์ผ๋ช… ์ฒ˜๋ฆฌ
import urllib.parse
filename = pdf_file.name
encoded_filename = urllib.parse.quote(filename)
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
background_tasks.add_task(cache_pdf, path)
# ์‘๋‹ต ํ—ค๋” ์„ค์ •
headers = {
"Content-Type": "application/pdf",
"Content-Disposition": f"inline; filename=\"{encoded_filename}\"; filename*=UTF-8''{encoded_filename}"
}
return Response(content=content, media_type="application/pdf", headers=headers)
except Exception as e:
import traceback
error_details = traceback.format_exc()
logger.error(f"PDF ์ฝ˜ํ…์ธ  ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
return JSONResponse(content={"error": str(e)}, status_code=500)
# HTML ํŒŒ์ผ ์ฝ๊ธฐ ํ•จ์ˆ˜
def get_html_content():
html_path = BASE / "flipbook_template.html"
if html_path.exists():
with open(html_path, "r", encoding="utf-8") as f:
return f.read()
return HTML # ๊ธฐ๋ณธ HTML ์‚ฌ์šฉ
# HTML ๋ฌธ์ž์—ด (์ด์ „์— ์—…๋ฐ์ดํŠธํ•œ HTML ๋‚ด์šฉ)
HTML = """
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>FlipBook Space</title>
<link rel="stylesheet" href="/static/flipbook.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="/static/three.js"></script>
<script src="/static/iscroll.js"></script>
<script src="/static/mark.js"></script>
<script src="/static/mod3d.js"></script>
<script src="/static/pdf.js"></script>
<script src="/static/flipbook.js"></script>
<script src="/static/flipbook.book3.js"></script>
<script src="/static/flipbook.scroll.js"></script>
<script src="/static/flipbook.swipe.js"></script>
<script src="/static/flipbook.webgl.js"></script>
<style>
/* ์ „์ฒด ์‚ฌ์ดํŠธ ํŒŒ์Šคํ…”ํ†ค ํ…Œ๋งˆ */
:root {
--primary-color: #a5d8ff; /* ํŒŒ์Šคํ…” ๋ธ”๋ฃจ */
--secondary-color: #ffd6e0; /* ํŒŒ์Šคํ…” ํ•‘ํฌ */
--tertiary-color: #c3fae8; /* ํŒŒ์Šคํ…” ๋ฏผํŠธ */
--accent-color: #d0bfff; /* ํŒŒ์Šคํ…” ํผํ”Œ */
--bg-color: #f8f9fa; /* ๋ฐ์€ ๋ฐฐ๊ฒฝ */
--text-color: #495057; /* ๋ถ€๋“œ๋Ÿฌ์šด ์–ด๋‘์šด ์ƒ‰ */
--card-bg: #ffffff; /* ์นด๋“œ ๋ฐฐ๊ฒฝ์ƒ‰ */
--shadow-sm: 0 2px 8px rgba(0,0,0,0.05);
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--transition: all 0.3s ease;
}
body {
margin: 0;
background: var(--bg-color);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: var(--text-color);
background-image: linear-gradient(120deg, var(--tertiary-color) 0%, var(--bg-color) 100%);
background-attachment: fixed;
}
header {
max-width: 1200px;
margin: 0 auto;
padding: 24px 30px;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
border-radius: 0 0 var(--radius-md) var(--radius-md);
box-shadow: var(--shadow-sm);
position: relative;
z-index: 10;
}
#homeBtn {
display: none;
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
cursor: pointer;
background: var(--primary-color);
color: white;
font-size: 22px;
margin-right: 16px;
box-shadow: var(--shadow-sm);
transition: var(--transition);
}
#homeBtn:hover {
background: #8bc5f8;
transform: scale(1.05);
box-shadow: var(--shadow-md);
}
h2 {
margin: 0;
font-size: 1.75rem;
font-weight: 600;
background: linear-gradient(120deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
letter-spacing: 0.5px;
}
#home, #viewerPage {
max-width: 1200px;
margin: 0 auto;
padding: 24px 30px 60px;
position: relative;
}
/* ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
.upload-container {
display: flex;
margin-bottom: 30px;
justify-content: center;
}
button.upload {
all: unset;
cursor: pointer;
padding: 12px 20px;
border-radius: var(--radius-md);
background: white;
margin: 0 10px;
font-weight: 500;
display: flex;
align-items: center;
box-shadow: var(--shadow-sm);
transition: var(--transition);
position: relative;
overflow: hidden;
}
button.upload::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(120deg, var(--primary-color) 0%, var(--secondary-color) 100%);
opacity: 0.08;
z-index: -1;
}
button.upload:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
button.upload:hover::before {
opacity: 0.15;
}
button.upload i {
margin-right: 8px;
font-size: 20px;
}
/* ๊ทธ๋ฆฌ๋“œ ๋ฐ ์นด๋“œ ์Šคํƒ€์ผ */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 24px;
margin-top: 36px;
}
.card {
background: var(--card-bg);
border-radius: var(--radius-md);
cursor: pointer;
box-shadow: var(--shadow-sm);
width: 100%;
height: 280px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: var(--transition);
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, var(--secondary-color) 0%, var(--primary-color) 100%);
opacity: 0.06;
z-index: 1;
}
.card::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 30%;
background: linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
z-index: 2;
}
.card img {
width: 65%;
height: auto;
object-fit: contain;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -65%);
border: 1px solid rgba(0,0,0,0.05);
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
z-index: 3;
transition: var(--transition);
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
}
.card:hover img {
transform: translate(-50%, -65%) scale(1.03);
box-shadow: 0 8px 20px rgba(0,0,0,0.12);
}
.card p {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
padding: 8px 16px;
border-radius: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
width: 80%;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
font-weight: 500;
color: var(--text-color);
z-index: 4;
transition: var(--transition);
}
.card:hover p {
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
/* ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ */
.cached-status {
position: absolute;
top: 10px;
right: 10px;
background: var(--accent-color);
color: white;
font-size: 11px;
padding: 3px 8px;
border-radius: 12px;
z-index: 5;
box-shadow: var(--shadow-sm);
}
/* ๋ทฐ์–ด ์Šคํƒ€์ผ */
#viewer {
width: 90%;
height: 90vh;
max-width: 90%;
margin: 0;
background: var(--card-bg);
border: none;
border-radius: var(--radius-lg);
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
box-shadow: var(--shadow-lg);
max-height: calc(90vh - 40px);
aspect-ratio: auto;
object-fit: contain;
overflow: hidden;
}
/* FlipBook ์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ */
.flipbook-container .fb3d-menu-bar {
z-index: 2000 !important;
opacity: 1 !important;
bottom: 0 !important;
background-color: rgba(255,255,255,0.9) !important;
backdrop-filter: blur(10px) !important;
border-radius: 0 0 var(--radius-lg) var(--radius-lg) !important;
padding: 12px 0 !important;
box-shadow: 0 -4px 20px rgba(0,0,0,0.1) !important;
}
.flipbook-container .fb3d-menu-bar > ul > li > img,
.flipbook-container .fb3d-menu-bar > ul > li > div {
opacity: 1 !important;
transform: scale(1.2) !important;
filter: drop-shadow(0 2px 3px rgba(0,0,0,0.1)) !important;
}
.flipbook-container .fb3d-menu-bar > ul > li {
margin: 0 12px !important;
}
/* ๋ฉ”๋‰ด ํˆดํŒ ์Šคํƒ€์ผ */
.flipbook-container .fb3d-menu-bar > ul > li > span {
background-color: rgba(0,0,0,0.7) !important;
color: white !important;
border-radius: var(--radius-sm) !important;
padding: 8px 12px !important;
font-size: 13px !important;
bottom: 55px !important;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
letter-spacing: 0.3px !important;
}
/* ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ๋ฐฐ๊ฒฝ ์˜ค๋ฒ„๋ ˆ์ด */
.viewer-mode {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
}
/* ๋ทฐ์–ด ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ */
#viewerPage {
background: transparent;
}
/* ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner {
border: 4px solid rgba(255,255,255,0.3);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
width: 50px;
height: 50px;
margin: 0 auto;
animation: spin 1.5s ease-in-out infinite;
}
.loading-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
padding: 30px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
z-index: 9999;
}
.loading-text {
margin-top: 20px;
font-size: 16px;
color: var(--text-color);
font-weight: 500;
}
/* ํŽ˜์ด์ง€ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
/* ์ถ”๊ฐ€ ์Šคํƒ€์ผ */
.section-title {
font-size: 1.3rem;
font-weight: 600;
margin: 30px 0 15px;
color: var(--text-color);
}
.no-projects {
text-align: center;
margin: 40px 0;
color: var(--text-color);
font-size: 16px;
}
/* ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” */
.progress-bar-container {
width: 100%;
height: 6px;
background-color: rgba(0,0,0,0.1);
border-radius: 3px;
margin-top: 15px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(to right, var(--primary-color), var(--accent-color));
border-radius: 3px;
transition: width 0.3s ease;
}
/* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
}
.card {
height: 240px;
}
header {
padding: 20px;
}
h2 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<header>
<button id="homeBtn" title="ํ™ˆ์œผ๋กœ"><i class="fas fa-home"></i></button>
<h2>My FlipBook Library</h2>
</header>
<section id="home" class="fade-in">
<div class="upload-container">
<button class="upload">
<i class="fas fa-images"></i> ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
<input id="imgInput" type="file" accept="image/*" multiple hidden>
</button>
<button class="upload">
<i class="fas fa-file-pdf"></i> PDF ์ถ”๊ฐ€
<input id="pdfInput" type="file" accept="application/pdf" hidden>
</button>
</div>
<div class="section-title">๋‚ด ํ”„๋กœ์ ํŠธ</div>
<div class="grid" id="grid">
<!-- ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
</div>
<div id="noProjects" class="no-projects" style="display: none;">
ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๋‚˜ PDF๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
</div>
</section>
<section id="viewerPage" style="display:none">
<div id="viewer"></div>
</section>
<script>
let projects=[], fb=null;
const grid=$id('grid'), viewer=$id('viewer');
pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
// ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
let serverProjects = [];
/* ๐Ÿ”Š ์˜ค๋””์˜ค unlock โ€“ ๋‚ด์žฅ Audio ์™€ ๊ฐ™์€ MP3 ๊ฒฝ๋กœ ์‚ฌ์šฉ */
['click','touchstart'].forEach(evt=>{
document.addEventListener(evt,function u(){new Audio('static/turnPage2.mp3')
.play().then(a=>a.pause()).catch(()=>{});document.removeEventListener(evt,u,{capture:true});},
{once:true,capture:true});
});
/* โ”€โ”€ ์œ ํ‹ธ โ”€โ”€ */
function $id(id){return document.getElementById(id)}
function addCard(i, thumb, title, isCached = false) {
const d = document.createElement('div');
d.className = 'card fade-in';
d.onclick = () => open(i);
// ์ œ๋ชฉ ์ฒ˜๋ฆฌ
const displayTitle = title ?
(title.length > 15 ? title.substring(0, 15) + '...' : title) :
'ํ”„๋กœ์ ํŠธ ' + (i+1);
// ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์ถ”๊ฐ€
const cachedBadge = isCached ?
'<div class="cached-status">์บ์‹œ๋จ</div>' : '';
d.innerHTML = `
<div class="card-inner">
${cachedBadge}
<img src="${thumb}" alt="${displayTitle}">
<p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
</div>
`;
grid.appendChild(d);
// ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์œผ๋ฉด 'ํ”„๋กœ์ ํŠธ ์—†์Œ' ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
$id('noProjects').style.display = 'none';
}
/* โ”€โ”€ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ โ”€โ”€ */
$id('imgInput').onchange=e=>{
const files=[...e.target.files]; if(!files.length) return;
// ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
showLoading("์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘...");
const pages=[],tot=files.length;let done=0;
files.forEach((f,i)=>{const r=new FileReader();r.onload=x=>{pages[i]={src:x.target.result,thumb:x.target.result};
if(++done===tot) {
save(pages, '์ด๋ฏธ์ง€ ์ปฌ๋ ‰์…˜');
hideLoading();
}
};r.readAsDataURL(f);});
};
/* โ”€โ”€ PDF ์—…๋กœ๋“œ โ”€โ”€ */
$id('pdfInput').onchange=e=>{
const file=e.target.files[0]; if(!file) return;
// ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
showLoading("PDF ๋กœ๋”ฉ ์ค‘...");
const fr=new FileReader();
fr.onload=v=>{
pdfjsLib.getDocument({data:v.target.result}).promise.then(async pdf=>{
const pages=[];
for(let p=1;p<=pdf.numPages;p++){
// ๋กœ๋”ฉ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
updateLoading(`PDF ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... (${p}/${pdf.numPages})`);
const pg=await pdf.getPage(p), vp=pg.getViewport({scale:1});
const c=document.createElement('canvas');c.width=vp.width;c.height=vp.height;
await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
pages.push({src:c.toDataURL(),thumb:c.toDataURL()});
}
hideLoading();
save(pages, file.name.replace('.pdf', ''));
}).catch(error => {
console.error("PDF ๋กœ๋”ฉ ์˜ค๋ฅ˜:", error);
hideLoading();
showError("PDF ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
});
};fr.readAsArrayBuffer(file);
};
/* โ”€โ”€ ํ”„๋กœ์ ํŠธ ์ €์žฅ โ”€โ”€ */
function save(pages, title, isCached = false){
const id=projects.push(pages)-1;
addCard(id, pages[0].thumb, title, isCached);
}
/* โ”€โ”€ ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ โ”€โ”€ */
async function loadServerPDFs() {
try {
// ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
if (document.querySelectorAll('.card').length === 0) {
showLoading("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘...");
}
// ๋จผ์ € ์บ์‹œ ์ƒํƒœ ํ™•์ธ
const cacheStatusRes = await fetch('/api/cache-status');
const cacheStatus = await cacheStatusRes.json();
// PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
const response = await fetch('/api/pdf-projects');
serverProjects = await response.json();
if (serverProjects.length === 0) {
hideLoading();
$id('noProjects').style.display = 'block';
return;
}
// ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์ธ๋„ค์ผ ์ƒ์„ฑ
for(let i = 0; i < serverProjects.length; i++) {
updateLoading(`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (${i+1}/${serverProjects.length})`);
const project = serverProjects[i];
const pdfName = project.name;
const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
// ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`);
const data = await response.json();
if(data.thumbnail) {
const pages = [{
src: data.thumbnail,
thumb: data.thumbnail,
path: project.path,
cached: isCached
}];
save(pages, project.name, isCached);
}
}
hideLoading();
// ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
if (document.querySelectorAll('.card').length === 0) {
$id('noProjects').style.display = 'block';
}
} catch(error) {
console.error('์„œ๋ฒ„ PDF ๋กœ๋“œ ์‹คํŒจ:', error);
hideLoading();
showError("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
}
/* โ”€โ”€ ์บ์‹œ ์ƒํƒœ ์ •๊ธฐ์ ์œผ๋กœ ํ™•์ธ โ”€โ”€ */
async function checkCacheStatus() {
try {
const response = await fetch('/api/cache-status');
const cacheStatus = await response.json();
// ํ˜„์žฌ ์นด๋“œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
const cards = document.querySelectorAll('.card');
for(let i = 0; i < cards.length; i++) {
if(projects[i] && projects[i][0] && projects[i][0].path) {
const pdfPath = projects[i][0].path;
const pdfName = pdfPath.split('/').pop().replace('.pdf', '');
// ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์—…๋ฐ์ดํŠธ
let badgeEl = cards[i].querySelector('.cached-status');
if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed") {
if(!badgeEl) {
badgeEl = document.createElement('div');
badgeEl.className = 'cached-status';
badgeEl.textContent = '์บ์‹œ๋จ';
cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
}
projects[i][0].cached = true;
} else if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "processing") {
if(!badgeEl) {
badgeEl = document.createElement('div');
badgeEl.className = 'cached-status';
cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
}
badgeEl.textContent = `${cacheStatus[pdfName].progress}%`;
badgeEl.style.background = 'var(--secondary-color)';
}
}
}
} catch(error) {
console.error('์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
}
}
/* โ”€โ”€ ์นด๋“œ โ†’ FlipBook โ”€โ”€ */
async function open(i) {
toggle(false);
const pages = projects[i];
// ๊ธฐ์กด FlipBook ์ •๋ฆฌ
if(fb) {
fb.destroy();
viewer.innerHTML = '';
}
// ์„œ๋ฒ„ PDF ๋˜๋Š” ๋กœ์ปฌ ํ”„๋กœ์ ํŠธ ์ฒ˜๋ฆฌ
if(pages[0].path) {
const pdfPath = pages[0].path;
// ์บ์‹œ ์—ฌ๋ถ€ ํ™•์ธ
if(pages[0].cached) {
// ์บ์‹œ๋œ PDF ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
showLoading("์บ์‹œ๋œ PDF ๋กœ๋”ฉ ์ค‘...");
try {
const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
const cachedData = await response.json();
if(cachedData.status === "completed" && cachedData.pages) {
hideLoading();
createFlipBook(cachedData.pages);
return;
}
} catch(error) {
console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
// ์บ์‹œ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์›๋ณธ PDF๋กœ ๋Œ€์ฒด
}
}
// ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
showLoading("PDF ์ค€๋น„ ์ค‘...");
try {
const response = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
const data = await response.json();
// ์บ์‹œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋œ ๊ฒฝ์šฐ
if(data.redirect) {
const redirectRes = await fetch(data.redirect);
const cachedData = await redirectRes.json();
if(cachedData.status === "completed" && cachedData.pages) {
hideLoading();
createFlipBook(cachedData.pages);
return;
} else if(cachedData.status === "processing") {
// ์บ์‹ฑ ์ง„ํ–‰ ์ค‘์ธ ๊ฒฝ์šฐ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ
updateLoading(`PDF ์บ์‹ฑ ์ค‘: ${cachedData.progress}%`, cachedData.progress);
// ์บ์‹ฑ ์™„๋ฃŒ ํ™•์ธ์„ ์œ„ํ•œ ํด๋ง
await waitForCaching(pdfPath);
return;
}
}
// ์›๋ณธ PDF ๋กœ๋“œ (ArrayBuffer ํ˜•ํƒœ)
const pdfResponse = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
const pdfData = await pdfResponse.arrayBuffer();
// PDF ๋กœ๋“œ ๋ฐ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
const pdf = await pdfjsLib.getDocument({data: pdfData}).promise;
const pdfPages = [];
for(let p = 1; p <= pdf.numPages; p++) {
updateLoading(`ํŽ˜์ด์ง€ ์ค€๋น„ ์ค‘... (${p}/${pdf.numPages})`);
const pg = await pdf.getPage(p);
const vp = pg.getViewport({scale: 1});
const c = document.createElement('canvas');
c.width = vp.width;
c.height = vp.height;
await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
}
hideLoading();
createFlipBook(pdfPages);
} catch(error) {
console.error('PDF ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
hideLoading();
showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
} else {
// ๋กœ์ปฌ ์—…๋กœ๋“œ๋œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰
createFlipBook(pages);
}
}
/* โ”€โ”€ ์บ์‹ฑ ์™„๋ฃŒ ๋Œ€๊ธฐ ํ•จ์ˆ˜ โ”€โ”€ */
async function waitForCaching(pdfPath) {
let retries = 0;
const maxRetries = 100; // ์ตœ๋Œ€ ํ™•์ธ ํšŸ์ˆ˜
const checkInterval = setInterval(async () => {
try {
const response = await fetch(`/api/cache-status?path=${encodeURIComponent(pdfPath)}`);
const status = await response.json();
if(status.status === "completed") {
// ์บ์‹ฑ ์™„๋ฃŒ
clearInterval(checkInterval);
hideLoading();
// ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋กœ FlipBook ์ƒ์„ฑ
const cachedRes = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
const cachedData = await cachedRes.json();
createFlipBook(cachedData.pages);
} else if(status.status === "processing") {
// ์ง„ํ–‰ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
updateLoading(`PDF ์บ์‹ฑ ์ค‘: ${status.progress}%`, status.progress);
} else if(status.status === "error" || retries >= maxRetries) {
// ์˜ค๋ฅ˜ ๋˜๋Š” ์‹œ๊ฐ„ ์ดˆ๊ณผ
clearInterval(checkInterval);
hideLoading();
showError("PDF ์บ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
}
retries++;
} catch(e) {
console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
retries++;
if(retries >= maxRetries) {
clearInterval(checkInterval);
hideLoading();
showError("PDF ์บ์‹ฑ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}
}
}, 1000); // 1์ดˆ๋งˆ๋‹ค ํ™•์ธ
}
function createFlipBook(pages) {
console.log('FlipBook ์ƒ์„ฑ ์‹œ์ž‘. ํŽ˜์ด์ง€ ์ˆ˜:', pages.length);
try {
// ํ™”๋ฉด ๋น„์œจ ๊ณ„์‚ฐ
const calculateAspectRatio = () => {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const aspectRatio = windowWidth / windowHeight;
// ๋„ˆ๋น„ ๋˜๋Š” ๋†’์ด ๊ธฐ์ค€์œผ๋กœ ์ตœ๋Œ€ 90% ์ œํ•œ
let width, height;
if (aspectRatio > 1) { // ๊ฐ€๋กœ ํ™”๋ฉด
height = Math.min(windowHeight * 0.9, windowHeight - 40);
width = height * aspectRatio * 0.8; // ๊ฐ€๋กœ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ๊ฐ„ ์ค„์ž„
if (width > windowWidth * 0.9) {
width = windowWidth * 0.9;
height = width / (aspectRatio * 0.8);
}
} else { // ์„ธ๋กœ ํ™”๋ฉด
width = Math.min(windowWidth * 0.9, windowWidth - 40);
height = width / aspectRatio * 0.9; // ์„ธ๋กœ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ๊ฐ„ ๋Š˜๋ฆผ
if (height > windowHeight * 0.9) {
height = windowHeight * 0.9;
width = height * aspectRatio * 0.9;
}
}
// ์ตœ์  ์‚ฌ์ด์ฆˆ ๋ฐ˜ํ™˜
return {
width: Math.round(width),
height: Math.round(height)
};
};
// ์ดˆ๊ธฐ ํ™”๋ฉด ๋น„์œจ ๊ณ„์‚ฐ
const size = calculateAspectRatio();
viewer.style.width = size.width + 'px';
viewer.style.height = size.height + 'px';
fb = new FlipBook(viewer, {
pages: pages,
viewMode: 'webgl',
autoSize: true,
flipDuration: 800,
backgroundColor: '#fff',
/* ๐Ÿ”Š ๋‚ด์žฅ ์‚ฌ์šด๋“œ */
sound: true,
assets: {flipMp3: 'static/turnPage2.mp3', hardFlipMp3: 'static/turnPage2.mp3'},
controlsProps: {
enableFullscreen: true,
enableToc: true,
enableDownload: false,
enablePrint: false,
enableZoom: true,
enableShare: false,
enableSearch: true,
enableAutoPlay: true,
enableAnnotation: false,
enableSound: true,
enableLightbox: false,
layout: 10, // ๋ ˆ์ด์•„์›ƒ ์˜ต์…˜
skin: 'light', // ์Šคํ‚จ ์Šคํƒ€์ผ
autoNavigationTime: 3600, // ์ž๋™ ๋„˜๊น€ ์‹œ๊ฐ„(์ดˆ)
hideControls: false, // ์ปจํŠธ๋กค ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
paddingTop: 10, // ์ƒ๋‹จ ํŒจ๋”ฉ
paddingLeft: 10, // ์ขŒ์ธก ํŒจ๋”ฉ
paddingRight: 10, // ์šฐ์ธก ํŒจ๋”ฉ
paddingBottom: 10, // ํ•˜๋‹จ ํŒจ๋”ฉ
pageTextureSize: 1024, // ํŽ˜์ด์ง€ ํ…์Šค์ฒ˜ ํฌ๊ธฐ
thumbnails: true, // ์„ฌ๋„ค์ผ ํ™œ์„ฑํ™”
autoHideControls: false, // ์ž๋™ ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
controlsTimeout: 8000 // ์ปจํŠธ๋กค ํ‘œ์‹œ ์‹œ๊ฐ„ ์—ฐ์žฅ
}
});
// ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ FlipBook ํฌ๊ธฐ ์กฐ์ •
window.addEventListener('resize', () => {
if (fb) {
const newSize = calculateAspectRatio();
viewer.style.width = newSize.width + 'px';
viewer.style.height = newSize.height + 'px';
fb.resize();
}
});
// FlipBook ์ƒ์„ฑ ํ›„ ์ปจํŠธ๋กค๋ฐ” ๊ฐ•์ œ ํ‘œ์‹œ
setTimeout(() => {
try {
// ์ปจํŠธ๋กค๋ฐ” ๊ด€๋ จ ์š”์†Œ ์ฐพ๊ธฐ ๋ฐ ์Šคํƒ€์ผ ์ ์šฉ
const menuBars = document.querySelectorAll('.flipbook-container .fb3d-menu-bar');
if (menuBars && menuBars.length > 0) {
menuBars.forEach(menuBar => {
menuBar.style.display = 'block';
menuBar.style.opacity = '1';
menuBar.style.visibility = 'visible';
menuBar.style.zIndex = '9999';
});
}
} catch (e) {
console.warn('์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜:', e);
}
}, 1000);
console.log('FlipBook ์ƒ์„ฑ ์™„๋ฃŒ');
} catch (error) {
console.error('FlipBook ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
showError("FlipBook์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
}
/* โ”€โ”€ ๋„ค๋น„๊ฒŒ์ด์…˜ โ”€โ”€ */
$id('homeBtn').onclick=()=>{
if(fb) {
fb.destroy();
viewer.innerHTML = '';
fb = null;
}
toggle(true);
};
function toggle(showHome){
$id('home').style.display=showHome?'block':'none';
$id('viewerPage').style.display=showHome?'none':'block';
$id('homeBtn').style.display=showHome?'none':'inline-block';
// ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
if(!showHome) {
document.body.classList.add('viewer-mode');
$id('homeBtn').style.position = 'fixed';
$id('homeBtn').style.top = '20px';
$id('homeBtn').style.left = '20px';
$id('homeBtn').style.zIndex = '9999';
$id('homeBtn').style.fontSize = '';
$id('homeBtn').style.width = '52px';
$id('homeBtn').style.height = '52px';
$id('homeBtn').style.boxShadow = '0 4px 15px rgba(0,0,0,0.15)';
} else {
document.body.classList.remove('viewer-mode');
$id('homeBtn').style.position = '';
$id('homeBtn').style.top = '';
$id('homeBtn').style.left = '';
$id('homeBtn').style.zIndex = '';
$id('homeBtn').style.fontSize = '';
$id('homeBtn').style.width = '';
$id('homeBtn').style.height = '';
$id('homeBtn').style.boxShadow = '';
}
}
/* -- ๋กœ๋”ฉ ๋ฐ ์˜ค๋ฅ˜ ํ‘œ์‹œ -- */
function showLoading(message, progress = -1) {
// ๊ธฐ์กด ๋กœ๋”ฉ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
hideLoading();
const loadingContainer = document.createElement('div');
loadingContainer.className = 'loading-container fade-in';
loadingContainer.id = 'loadingContainer';
let progressBarHtml = '';
if (progress >= 0) {
progressBarHtml = `
<div class="progress-bar-container">
<div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>
</div>
`;
}
loadingContainer.innerHTML = `
<div class="loading-spinner"></div>
<p class="loading-text" id="loadingText">${message || '๋กœ๋”ฉ ์ค‘...'}</p>
${progressBarHtml}
`;
document.body.appendChild(loadingContainer);
}
function updateLoading(message, progress = -1) {
const loadingText = $id('loadingText');
if (loadingText) {
loadingText.textContent = message;
}
if (progress >= 0) {
let progressBar = $id('progressBar');
if (!progressBar) {
const loadingContainer = $id('loadingContainer');
if (loadingContainer) {
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-bar-container';
progressContainer.innerHTML = `<div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>`;
loadingContainer.appendChild(progressContainer);
progressBar = $id('progressBar');
}
} else {
progressBar.style.width = `${progress}%`;
}
}
}
function hideLoading() {
const loadingContainer = $id('loadingContainer');
if (loadingContainer) {
loadingContainer.remove();
}
}
function showError(message) {
// ๊ธฐ์กด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
const existingError = $id('errorContainer');
if (existingError) {
existingError.remove();
}
const errorContainer = document.createElement('div');
errorContainer.className = 'loading-container fade-in';
errorContainer.id = 'errorContainer';
errorContainer.innerHTML = `
<p class="loading-text" style="color: #e74c3c;">${message}</p>
<button id="errorCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">ํ™•์ธ</button>
`;
document.body.appendChild(errorContainer);
// ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
$id('errorCloseBtn').onclick = () => {
errorContainer.remove();
};
// 5์ดˆ ํ›„ ์ž๋™์œผ๋กœ ๋‹ซ๊ธฐ
setTimeout(() => {
if ($id('errorContainer')) {
$id('errorContainer').remove();
}
}, 5000);
}
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
window.addEventListener('DOMContentLoaded', () => {
loadServerPDFs();
// ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ (5์ดˆ๋งˆ๋‹ค)
setInterval(checkCacheStatus, 5000);
});
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
async def root():
return get_html_content()
if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))