AI-BOOK / app.py
ginipick's picture
Update app.py
ad98527 verified
raw
history blame
135 kB
from fastapi import FastAPI, BackgroundTasks, UploadFile, File, Form, Request, Query
from fastapi.responses import HTMLResponse, JSONResponse, Response, RedirectResponse
from fastapi.staticfiles import StaticFiles
import pathlib, os, uvicorn, base64, json, shutil, uuid, time, urllib.parse
from typing import Dict, List, Any, Optional
import asyncio
import logging
import threading
import concurrent.futures
from openai import OpenAI
import fitz # PyMuPDF
import tempfile
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
import io
import docx2txt
# ๋กœ๊น… ์„ค์ •
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)
# ์˜๊ตฌ PDF ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • (Hugging Face ์˜๊ตฌ ๋””์Šคํฌ)
PERMANENT_PDF_DIR = pathlib.Path("/data/pdfs") if os.path.exists("/data") else BASE / "permanent_pdfs"
if not PERMANENT_PDF_DIR.exists():
PERMANENT_PDF_DIR.mkdir(parents=True)
# ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
CACHE_DIR = BASE / "cache"
if not CACHE_DIR.exists():
CACHE_DIR.mkdir(parents=True)
# PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋””๋ ‰ํ† ๋ฆฌ ๋ฐ ํŒŒ์ผ ์„ค์ •
METADATA_DIR = pathlib.Path("/data/metadata") if os.path.exists("/data") else BASE / "metadata"
if not METADATA_DIR.exists():
METADATA_DIR.mkdir(parents=True)
PDF_METADATA_FILE = METADATA_DIR / "pdf_metadata.json"
# ์ž„๋ฒ ๋”ฉ ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
EMBEDDING_DIR = pathlib.Path("/data/embeddings") if os.path.exists("/data") else BASE / "embeddings"
if not EMBEDDING_DIR.exists():
EMBEDDING_DIR.mkdir(parents=True)
# ๊ด€๋ฆฌ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ
ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ, ๊ธฐ๋ณธ๊ฐ’์€ ํ…Œ์ŠคํŠธ์šฉ
# OpenAI API ํ‚ค ์„ค์ •
OPENAI_API_KEY = os.getenv("LLM_API", "")
# API ํ‚ค๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋น„์–ด์žˆ์„ ๋•Œ ํ”Œ๋ž˜๊ทธ ์„ค์ •
HAS_VALID_API_KEY = bool(OPENAI_API_KEY and OPENAI_API_KEY.strip())
if HAS_VALID_API_KEY:
try:
openai_client = OpenAI(api_key=OPENAI_API_KEY, timeout=30.0)
logger.info("OpenAI ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
except Exception as e:
logger.error(f"OpenAI ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
HAS_VALID_API_KEY = False
else:
logger.warning("์œ ํšจํ•œ OpenAI API ํ‚ค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. AI ๊ธฐ๋Šฅ์ด ์ œํ•œ๋ฉ๋‹ˆ๋‹ค.")
openai_client = None
# ์ „์—ญ ์บ์‹œ ๊ฐ์ฒด
pdf_cache: Dict[str, Dict[str, Any]] = {}
# ์บ์‹ฑ ๋ฝ
cache_locks = {}
# PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (ID to ๊ฒฝ๋กœ ๋งคํ•‘)
pdf_metadata: Dict[str, str] = {}
# PDF ์ž„๋ฒ ๋”ฉ ์บ์‹œ
pdf_embeddings: Dict[str, Dict[str, Any]] = {}
# PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
def load_pdf_metadata():
global pdf_metadata
if PDF_METADATA_FILE.exists():
try:
with open(PDF_METADATA_FILE, "r") as f:
pdf_metadata = json.load(f)
logger.info(f"PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ: {len(pdf_metadata)} ํ•ญ๋ชฉ")
except Exception as e:
logger.error(f"๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜: {e}")
pdf_metadata = {}
else:
pdf_metadata = {}
# PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
def save_pdf_metadata():
try:
with open(PDF_METADATA_FILE, "w") as f:
json.dump(pdf_metadata, f)
except Exception as e:
logger.error(f"๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ ์˜ค๋ฅ˜: {e}")
# PDF ID ์ƒ์„ฑ (ํŒŒ์ผ๋ช… + ํƒ€์ž„์Šคํƒฌํ”„ ๊ธฐ๋ฐ˜) - ๋” ๋‹จ์ˆœํ•˜๊ณ  ์•ˆ์ „ํ•œ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝ
def generate_pdf_id(filename: str) -> str:
# ํŒŒ์ผ๋ช…์—์„œ ํ™•์žฅ์ž ์ œ๊ฑฐ
base_name = os.path.splitext(filename)[0]
# ์•ˆ์ „ํ•œ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (URL ์ธ์ฝ”๋”ฉ ๋Œ€์‹  ์ง์ ‘ ๋ณ€ํ™˜)
import re
safe_name = re.sub(r'[^\w\-_]', '_', base_name.replace(" ", "_"))
# ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€๋กœ ๊ณ ์œ ์„ฑ ๋ณด์žฅ
timestamp = int(time.time())
# ์งง์€ ์ž„์˜ ๋ฌธ์ž์—ด ์ถ”๊ฐ€
random_suffix = uuid.uuid4().hex[:6]
return f"{safe_name}_{timestamp}_{random_suffix}"
# 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 get_permanent_pdf_files():
pdf_files = []
if PERMANENT_PDF_DIR.exists():
pdf_files = [f for f in PERMANENT_PDF_DIR.glob("*.pdf")]
return pdf_files
# PDF ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
def generate_pdf_projects():
projects_data = []
# ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์™€ ์˜๊ตฌ ์ €์žฅ์†Œ์˜ ํŒŒ์ผ๋“ค ๊ฐ€์ ธ์˜ค๊ธฐ
pdf_files = get_pdf_files()
permanent_pdf_files = get_permanent_pdf_files()
# ๋ชจ๋“  ํŒŒ์ผ ํ•ฉ์น˜๊ธฐ (ํŒŒ์ผ๋ช… ๊ธฐ์ค€์œผ๋กœ ์ค‘๋ณต ์ œ๊ฑฐ)
unique_files = {}
# ๋จผ์ € ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์˜ ํŒŒ์ผ๋“ค ์ถ”๊ฐ€
for file in pdf_files:
unique_files[file.name] = file
# ์˜๊ตฌ ์ €์žฅ์†Œ์˜ ํŒŒ์ผ๋“ค ์ถ”๊ฐ€ (๋™์ผ ํŒŒ์ผ๋ช…์ด ์žˆ์œผ๋ฉด ์˜๊ตฌ ์ €์žฅ์†Œ ํŒŒ์ผ ์šฐ์„ )
for file in permanent_pdf_files:
unique_files[file.name] = file
# ์ค‘๋ณต ์ œ๊ฑฐ๋œ ํŒŒ์ผ๋“ค๋กœ ํ”„๋กœ์ ํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
for pdf_file in unique_files.values():
# ํ•ด๋‹น ํŒŒ์ผ์˜ PDF ID ์ฐพ๊ธฐ
pdf_id = None
for pid, path in pdf_metadata.items():
if os.path.basename(path) == pdf_file.name:
pdf_id = pid
break
# ID๊ฐ€ ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑํ•˜๊ณ  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€
if not pdf_id:
pdf_id = generate_pdf_id(pdf_file.name)
pdf_metadata[pdf_id] = str(pdf_file)
save_pdf_metadata()
projects_data.append({
"path": str(pdf_file),
"name": pdf_file.stem,
"id": pdf_id,
"cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
})
return projects_data
# ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ ์ƒ์„ฑ
def get_cache_path(pdf_name: str):
return CACHE_DIR / f"{pdf_name}_cache.json"
# ์ž„๋ฒ ๋”ฉ ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ ์ƒ์„ฑ
def get_embedding_path(pdf_id: str):
return EMBEDDING_DIR / f"{pdf_id}_embedding.json"
# PDF ํ…์ŠคํŠธ ์ถ”์ถœ ํ•จ์ˆ˜
def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]:
try:
doc = fitz.open(pdf_path)
chunks = []
for page_num in range(len(doc)):
page = doc[page_num]
text = page.get_text()
# ํŽ˜์ด์ง€ ํ…์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ ์ถ”๊ฐ€
if text.strip():
chunks.append({
"page": page_num + 1,
"text": text,
"chunk_id": f"page_{page_num + 1}"
})
return chunks
except Exception as e:
logger.error(f"PDF ํ…์ŠคํŠธ ์ถ”์ถœ ์˜ค๋ฅ˜: {e}")
return []
# PDF ID๋กœ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ๋˜๋Š” ๊ฐ€์ ธ์˜ค๊ธฐ
async def get_pdf_embedding(pdf_id: str) -> Dict[str, Any]:
try:
# ์ž„๋ฒ ๋”ฉ ์บ์‹œ ํ™•์ธ
embedding_path = get_embedding_path(pdf_id)
if embedding_path.exists():
try:
with open(embedding_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
logger.error(f"์ž„๋ฒ ๋”ฉ ์บ์‹œ ๋กœ๋“œ ์˜ค๋ฅ˜: {e}")
# PDF ๊ฒฝ๋กœ ์ฐพ๊ธฐ
pdf_path = get_pdf_path_by_id(pdf_id)
if not pdf_path:
raise ValueError(f"PDF ID {pdf_id}์— ํ•ด๋‹นํ•˜๋Š” ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
# ํ…์ŠคํŠธ ์ถ”์ถœ
chunks = extract_pdf_text(pdf_path)
if not chunks:
raise ValueError(f"PDF์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {pdf_path}")
# ์ž„๋ฒ ๋”ฉ ์ €์žฅ ๋ฐ ๋ฐ˜ํ™˜
embedding_data = {
"pdf_id": pdf_id,
"pdf_path": pdf_path,
"chunks": chunks,
"created_at": time.time()
}
# ์ž„๋ฒ ๋”ฉ ์บ์‹œ ์ €์žฅ
with open(embedding_path, "w", encoding="utf-8") as f:
json.dump(embedding_data, f, ensure_ascii=False)
return embedding_data
except Exception as e:
logger.error(f"PDF ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
return {"error": str(e), "pdf_id": pdf_id}
# PDF ๋‚ด์šฉ ๊ธฐ๋ฐ˜ ์งˆ์˜์‘๋‹ต
# PDF ๋‚ด์šฉ ๊ธฐ๋ฐ˜ ์งˆ์˜์‘๋‹ต ํ•จ์ˆ˜ ๊ฐœ์„ 
async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
try:
# API ํ‚ค๊ฐ€ ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ
if not HAS_VALID_API_KEY or not openai_client:
return {
"error": "OpenAI API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.",
"answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ AI ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”."
}
# ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
embedding_data = await get_pdf_embedding(pdf_id)
if "error" in embedding_data:
return {"error": embedding_data["error"]}
# ์ฒญํฌ ํ…์ŠคํŠธ ๋ชจ์œผ๊ธฐ (์ž„์‹œ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ „์ฒด ํ…์ŠคํŠธ ์‚ฌ์šฉ)
all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
# ์ปจํ…์ŠคํŠธ ํฌ๊ธฐ๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ํ…์ŠคํŠธ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ๋ฉด ์•ž๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉ
max_context_length = 60000 # ํ† ํฐ ์ˆ˜๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž ์ˆ˜ ๊ธฐ์ค€ (๋Œ€๋žต์ ์ธ ์ œํ•œ)
if len(all_text) > max_context_length:
all_text = all_text[:max_context_length] + "...(์ดํ•˜ ์ƒ๋žต)"
# ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์ค€๋น„
system_prompt = """
๋‹น์‹ ์€ PDF ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜๋Š” ๋„์šฐ๋ฏธ์ž…๋‹ˆ๋‹ค. ์ œ๊ณต๋œ PDF ์ปจํ…์ŠคํŠธ ์ •๋ณด๋งŒ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”.
์ปจํ…์ŠคํŠธ์— ๊ด€๋ จ ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, '์ œ๊ณต๋œ PDF์—์„œ ํ•ด๋‹น ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'๋ผ๊ณ  ์†”์งํžˆ ๋‹ตํ•˜์„ธ์š”.
๋‹ต๋ณ€์€ ๋ช…ํ™•ํ•˜๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•˜๊ณ , ๊ด€๋ จ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ์ธ์šฉํ•˜์„ธ์š”. ๋ฐ˜๋“œ์‹œ ๊ณต์†ํ•œ ๋งํˆฌ๋กœ ์นœ์ ˆํ•˜๊ฒŒ ์‘๋‹ตํ•˜์„ธ์š”.
"""
# gpt-4.1-mini ๋ชจ๋ธ ์‚ฌ์šฉ
try:
# ํƒ€์ž„์•„์›ƒ ๋ฐ ์žฌ์‹œ๋„ ์„ค์ • ๊ฐœ์„ 
for attempt in range(3): # ์ตœ๋Œ€ 3๋ฒˆ ์žฌ์‹œ๋„
try:
response = openai_client.chat.completions.create(
model="gpt-4.1-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"๋‹ค์Œ PDF ๋‚ด์šฉ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•ด์ฃผ์„ธ์š”.\n\nPDF ๋‚ด์šฉ:\n{all_text}\n\n์งˆ๋ฌธ: {query}"}
],
temperature=0.7,
max_tokens=2048,
timeout=30.0 # 30์ดˆ ํƒ€์ž„์•„์›ƒ
)
answer = response.choices[0].message.content
return {
"answer": answer,
"pdf_id": pdf_id,
"query": query
}
except Exception as api_error:
logger.error(f"OpenAI API ํ˜ธ์ถœ ์˜ค๋ฅ˜ (์‹œ๋„ {attempt+1}/3): {api_error}")
if attempt == 2: # ๋งˆ์ง€๋ง‰ ์‹œ๋„์—์„œ๋„ ์‹คํŒจ
raise api_error
await asyncio.sleep(1 * (attempt + 1)) # ์žฌ์‹œ๋„ ๊ฐ„ ์ง€์—ฐ ์‹œ๊ฐ„ ์ฆ๊ฐ€
# ์—ฌ๊ธฐ๊นŒ์ง€ ๋„๋‹ฌํ•˜์ง€ ์•Š์•„์•ผ ํ•จ
raise Exception("API ํ˜ธ์ถœ ์žฌ์‹œ๋„ ๋ชจ๋‘ ์‹คํŒจ")
except Exception as api_error:
logger.error(f"OpenAI API ํ˜ธ์ถœ ์ตœ์ข… ์˜ค๋ฅ˜: {api_error}")
# ์˜ค๋ฅ˜ ์œ ํ˜•์— ๋”ฐ๋ฅธ ๋” ๋ช…ํ™•ํ•œ ๋ฉ”์‹œ์ง€ ์ œ๊ณต
error_message = str(api_error)
if "Connection" in error_message:
return {"error": "OpenAI ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜์„ธ์š”."}
elif "Unauthorized" in error_message or "Authentication" in error_message:
return {"error": "API ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."}
elif "Rate limit" in error_message:
return {"error": "API ํ˜ธ์ถœ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”."}
else:
return {"error": f"AI ์‘๋‹ต ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {error_message}"}
except Exception as e:
logger.error(f"์งˆ์˜์‘๋‹ต ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
return {"error": str(e)}
# PDF ์š”์•ฝ ์ƒ์„ฑ
# PDF ์š”์•ฝ ์ƒ์„ฑ ํ•จ์ˆ˜ ๊ฐœ์„ 
async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
try:
# API ํ‚ค๊ฐ€ ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ
if not HAS_VALID_API_KEY or not openai_client:
return {
"error": "OpenAI API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. 'LLM_API' ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ํ™•์ธํ•˜์„ธ์š”.",
"summary": "API ํ‚ค๊ฐ€ ์—†์–ด ์š”์•ฝ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”."
}
# ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
embedding_data = await get_pdf_embedding(pdf_id)
if "error" in embedding_data:
return {"error": embedding_data["error"], "summary": "PDF์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}
# ์ฒญํฌ ํ…์ŠคํŠธ ๋ชจ์œผ๊ธฐ (์ œํ•œ๋œ ๊ธธ์ด)
all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
# ์ปจํ…์ŠคํŠธ ํฌ๊ธฐ๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ํ…์ŠคํŠธ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ๋ฉด ์•ž๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉ
max_context_length = 60000 # ํ† ํฐ ์ˆ˜๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž ์ˆ˜ ๊ธฐ์ค€ (๋Œ€๋žต์ ์ธ ์ œํ•œ)
if len(all_text) > max_context_length:
all_text = all_text[:max_context_length] + "...(์ดํ•˜ ์ƒ๋žต)"
# OpenAI API ํ˜ธ์ถœ
try:
# ํƒ€์ž„์•„์›ƒ ๋ฐ ์žฌ์‹œ๋„ ์„ค์ • ๊ฐœ์„ 
for attempt in range(3): # ์ตœ๋Œ€ 3๋ฒˆ ์žฌ์‹œ๋„
try:
response = openai_client.chat.completions.create(
model="gpt-4.1-mini",
messages=[
{"role": "system", "content": "๋‹ค์Œ PDF ๋‚ด์šฉ์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์š”์•ฝํ•ด์ฃผ์„ธ์š”. ํ•ต์‹ฌ ์ฃผ์ œ์™€ ์ฃผ์š” ํฌ์ธํŠธ๋ฅผ ํฌํ•จํ•œ ์š”์•ฝ์„ 500์ž ์ด๋‚ด๋กœ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”."},
{"role": "user", "content": f"PDF ๋‚ด์šฉ:\n{all_text}"}
],
temperature=0.7,
max_tokens=1024,
timeout=30.0 # 30์ดˆ ํƒ€์ž„์•„์›ƒ
)
summary = response.choices[0].message.content
return {
"summary": summary,
"pdf_id": pdf_id
}
except Exception as api_error:
logger.error(f"OpenAI API ํ˜ธ์ถœ ์˜ค๋ฅ˜ (์‹œ๋„ {attempt+1}/3): {api_error}")
if attempt == 2: # ๋งˆ์ง€๋ง‰ ์‹œ๋„์—์„œ๋„ ์‹คํŒจ
raise api_error
await asyncio.sleep(1 * (attempt + 1)) # ์žฌ์‹œ๋„ ๊ฐ„ ์ง€์—ฐ ์‹œ๊ฐ„ ์ฆ๊ฐ€
# ์—ฌ๊ธฐ๊นŒ์ง€ ๋„๋‹ฌํ•˜์ง€ ์•Š์•„์•ผ ํ•จ
raise Exception("API ํ˜ธ์ถœ ์žฌ์‹œ๋„ ๋ชจ๋‘ ์‹คํŒจ")
except Exception as api_error:
logger.error(f"OpenAI API ํ˜ธ์ถœ ์ตœ์ข… ์˜ค๋ฅ˜: {api_error}")
# ์˜ค๋ฅ˜ ์œ ํ˜•์— ๋”ฐ๋ฅธ ๋” ๋ช…ํ™•ํ•œ ๋ฉ”์‹œ์ง€ ์ œ๊ณต
error_message = str(api_error)
if "Connection" in error_message:
return {"error": "OpenAI ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜์„ธ์š”.", "pdf_id": pdf_id}
elif "Unauthorized" in error_message or "Authentication" in error_message:
return {"error": "API ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", "pdf_id": pdf_id}
elif "Rate limit" in error_message:
return {"error": "API ํ˜ธ์ถœ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.", "pdf_id": pdf_id}
else:
return {"error": f"AI ์š”์•ฝ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {error_message}", "pdf_id": pdf_id}
except Exception as e:
logger.error(f"PDF ์š”์•ฝ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
return {
"error": str(e),
"summary": "PDF ์š”์•ฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. PDF ํŽ˜์ด์ง€ ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ๊ฑฐ๋‚˜ ํ˜•์‹์ด ์ง€์›๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."
}
# ์ตœ์ ํ™”๋œ 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 not in cache_locks:
cache_locks[pdf_name] = threading.Lock()
# ์ด๋ฏธ ์บ์‹ฑ ์ค‘์ด๊ฑฐ๋‚˜ ์บ์‹ฑ ์™„๋ฃŒ๋œ PDF๋Š” ๊ฑด๋„ˆ๋›ฐ๊ธฐ
if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
logger.info(f"PDF {pdf_name} ์ด๋ฏธ ์บ์‹ฑ ์™„๋ฃŒ ๋˜๋Š” ์ง„ํ–‰ ์ค‘")
return
with cache_locks[pdf_name]:
# ์ด์ค‘ ์ฒดํฌ - ๋ฝ ํš๋“ ํ›„ ๋‹ค์‹œ ํ™•์ธ
if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
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)
if cached_data.get("status") == "completed" and cached_data.get("pages"):
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
# ๋ฏธ๋ฆฌ ์ธ๋„ค์ผ๋งŒ ๋จผ์ € ์ƒ์„ฑ (๋น ๋ฅธ UI ๋กœ๋”ฉ์šฉ)
if total_pages > 0:
# ์ฒซ ํŽ˜์ด์ง€ ์ธ๋„ค์ผ ์ƒ์„ฑ
page = doc[0]
pix_thumb = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2)) # ๋” ์ž‘์€ ์ธ๋„ค์ผ
thumb_data = pix_thumb.tobytes("png")
b64_thumb = base64.b64encode(thumb_data).decode('utf-8')
thumb_src = f"data:image/png;base64,{b64_thumb}"
# ์ธ๋„ค์ผ ํŽ˜์ด์ง€๋งŒ ๋จผ์ € ์บ์‹œ
pdf_cache[pdf_name]["pages"] = [{"thumb": thumb_src, "src": ""}]
pdf_cache[pdf_name]["progress"] = 1
pdf_cache[pdf_name]["total_pages"] = total_pages
# ์ด๋ฏธ์ง€ ํ•ด์ƒ๋„ ๋ฐ ์••์ถ• ํ’ˆ์งˆ ์„ค์ • (์„ฑ๋Šฅ ์ตœ์ ํ™”)
scale_factor = 1.0 # ๊ธฐ๋ณธ ํ•ด์ƒ๋„ (๋‚ฎ์ถœ์ˆ˜๋ก ๋กœ๋”ฉ ๋น ๋ฆ„)
jpeg_quality = 80 # JPEG ํ’ˆ์งˆ (๋‚ฎ์ถœ์ˆ˜๋ก ์šฉ๋Ÿ‰ ์ž‘์•„์ง)
# ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ ์ž‘์—…์ž ํ•จ์ˆ˜ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ์šฉ)
def process_page(page_num):
try:
page = doc[page_num]
# ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ ์‹œ ๋งคํŠธ๋ฆญ์Šค ์Šค์ผ€์ผ๋ง ์ ์šฉ (์„ฑ๋Šฅ ์ตœ์ ํ™”)
pix = page.get_pixmap(matrix=fitz.Matrix(scale_factor, scale_factor))
# JPEG ํ˜•์‹์œผ๋กœ ์ธ์ฝ”๋”ฉ (PNG๋ณด๋‹ค ํฌ๊ธฐ ์ž‘์Œ)
img_data = pix.tobytes("jpeg", jpeg_quality)
b64_img = base64.b64encode(img_data).decode('utf-8')
img_src = f"data:image/jpeg;base64,{b64_img}"
# ์ธ๋„ค์ผ (์ฒซ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹ˆ๋ฉด ๋นˆ ๋ฌธ์ž์—ด)
thumb_src = "" if page_num > 0 else pdf_cache[pdf_name]["pages"][0]["thumb"]
return {
"page_num": page_num,
"src": img_src,
"thumb": thumb_src
}
except Exception as e:
logger.error(f"ํŽ˜์ด์ง€ {page_num} ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
return {
"page_num": page_num,
"src": "",
"thumb": "",
"error": str(e)
}
# ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ๋ชจ๋“  ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ
pages = [None] * total_pages
processed_count = 0
# ํŽ˜์ด์ง€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ)
batch_size = 5 # ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ํŽ˜์ด์ง€ ์ˆ˜
for batch_start in range(0, total_pages, batch_size):
batch_end = min(batch_start + batch_size, total_pages)
current_batch = list(range(batch_start, batch_end))
# ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ๋ฐฐ์น˜ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
with concurrent.futures.ThreadPoolExecutor(max_workers=min(5, batch_size)) as executor:
batch_results = list(executor.map(process_page, current_batch))
# ๊ฒฐ๊ณผ ์ €์žฅ
for result in batch_results:
page_num = result["page_num"]
pages[page_num] = {
"src": result["src"],
"thumb": result["thumb"]
}
processed_count += 1
progress = round(processed_count / total_pages * 100)
pdf_cache[pdf_name]["progress"] = progress
# ์ค‘๊ฐ„ ์ €์žฅ
pdf_cache[pdf_name]["pages"] = pages
try:
with open(cache_path, "w") as cache_file:
json.dump({
"status": "processing",
"progress": pdf_cache[pdf_name]["progress"],
"pages": pdf_cache[pdf_name]["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 ID๋กœ PDF ๊ฒฝ๋กœ ์ฐพ๊ธฐ (๊ฐœ์„ ๋œ ๊ฒ€์ƒ‰ ๋กœ์ง)
def get_pdf_path_by_id(pdf_id: str) -> str:
logger.info(f"PDF ID๋กœ ํŒŒ์ผ ์กฐํšŒ: {pdf_id}")
# 1. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ์ง์ ‘ ID๋กœ ๊ฒ€์ƒ‰
if pdf_id in pdf_metadata:
path = pdf_metadata[pdf_id]
# ํŒŒ์ผ ์กด์žฌ ํ™•์ธ
if os.path.exists(path):
return path
# ํŒŒ์ผ์ด ์ด๋™ํ–ˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํŒŒ์ผ๋ช…์œผ๋กœ ๊ฒ€์ƒ‰
filename = os.path.basename(path)
# ์˜๊ตฌ ์ €์žฅ์†Œ์—์„œ ๊ฒ€์ƒ‰
perm_path = PERMANENT_PDF_DIR / filename
if perm_path.exists():
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
pdf_metadata[pdf_id] = str(perm_path)
save_pdf_metadata()
return str(perm_path)
# ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ๊ฒ€์ƒ‰
main_path = PDF_DIR / filename
if main_path.exists():
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
pdf_metadata[pdf_id] = str(main_path)
save_pdf_metadata()
return str(main_path)
# 2. ํŒŒ์ผ๋ช… ๋ถ€๋ถ„๋งŒ ์ถ”์ถœํ•˜์—ฌ ๋ชจ๋“  PDF ํŒŒ์ผ ๊ฒ€์ƒ‰
try:
# ID ํ˜•์‹: filename_timestamp_random
# ํŒŒ์ผ๋ช… ๋ถ€๋ถ„๋งŒ ์ถ”์ถœ
name_part = pdf_id.split('_')[0] if '_' in pdf_id else pdf_id
# ๋ชจ๋“  PDF ํŒŒ์ผ ๊ฒ€์ƒ‰
for file_path in get_pdf_files() + get_permanent_pdf_files():
# ํŒŒ์ผ๋ช…์ด ID์˜ ์‹œ์ž‘ ๋ถ€๋ถ„๊ณผ ์ผ์น˜ํ•˜๋ฉด
file_basename = os.path.basename(file_path)
if file_basename.startswith(name_part) or file_path.stem.startswith(name_part):
# ID ๋งคํ•‘ ์—…๋ฐ์ดํŠธ
pdf_metadata[pdf_id] = str(file_path)
save_pdf_metadata()
return str(file_path)
except Exception as e:
logger.error(f"ํŒŒ์ผ๋ช… ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜: {e}")
# 3. ๋ชจ๋“  PDF ํŒŒ์ผ์— ๋Œ€ํ•ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ™•์ธ
for pid, path in pdf_metadata.items():
if os.path.exists(path):
file_basename = os.path.basename(path)
# ์œ ์‚ฌํ•œ ํŒŒ์ผ๋ช…์„ ๊ฐ€์ง„ ๊ฒฝ์šฐ
if pdf_id in pid or pid in pdf_id:
pdf_metadata[pdf_id] = path
save_pdf_metadata()
return path
return None
# ์‹œ์ž‘ ์‹œ ๋ชจ๋“  PDF ํŒŒ์ผ ์บ์‹ฑ
async def init_cache_all_pdfs():
logger.info("PDF ์บ์‹ฑ ์ž‘์—… ์‹œ์ž‘")
# PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
load_pdf_metadata()
# ๋ฉ”์ธ ๋ฐ ์˜๊ตฌ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ PDF ํŒŒ์ผ ๋ชจ๋‘ ๊ฐ€์ ธ์˜ค๊ธฐ
pdf_files = get_pdf_files() + get_permanent_pdf_files()
# ์ค‘๋ณต ์ œ๊ฑฐ
unique_pdf_paths = set(str(p) for p in pdf_files)
pdf_files = [pathlib.Path(p) for p in unique_pdf_paths]
# ํŒŒ์ผ ๊ธฐ๋ฐ˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
for pdf_file in pdf_files:
# ID๊ฐ€ ์—†๋Š” ํŒŒ์ผ์— ๋Œ€ํ•ด ID ์ƒ์„ฑ
found = False
for pid, path in pdf_metadata.items():
if os.path.basename(path) == pdf_file.name:
found = True
# ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
if not os.path.exists(path):
pdf_metadata[pid] = str(pdf_file)
break
if not found:
pdf_id = generate_pdf_id(pdf_file.name)
pdf_metadata[pdf_id] = str(pdf_file)
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
save_pdf_metadata()
# ์ด๋ฏธ ์บ์‹œ๋œ 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:
cached_data = json.load(f)
if cached_data.get("status") == "completed" and cached_data.get("pages"):
pdf_cache[pdf_name] = cached_data
pdf_cache[pdf_name]["status"] = "completed"
logger.info(f"๊ธฐ์กด ์บ์‹œ ๋กœ๋“œ: {pdf_name}")
except Exception as e:
logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}")
# ์บ์‹ฑ๋˜์ง€ ์•Š์€ PDF ํŒŒ์ผ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
await asyncio.gather(*[asyncio.create_task(cache_pdf(str(pdf_file)))
for pdf_file in pdf_files
if pdf_file.stem not in pdf_cache
or pdf_cache[pdf_file.stem].get("status") != "completed"])
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹œ์ž‘ ํ•จ์ˆ˜
@app.on_event("startup")
async def startup_event():
# PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋กœ๋“œ
load_pdf_metadata()
# ๋ˆ„๋ฝ๋œ PDF ํŒŒ์ผ์— ๋Œ€ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ
for pdf_file in get_pdf_files() + get_permanent_pdf_files():
found = False
for pid, path in pdf_metadata.items():
if os.path.basename(path) == pdf_file.name:
found = True
# ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ
if not os.path.exists(path):
pdf_metadata[pid] = str(pdf_file)
break
if not found:
# ์ƒˆ ID ์ƒ์„ฑ ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€
pdf_id = generate_pdf_id(pdf_file.name)
pdf_metadata[pdf_id] = str(pdf_file)
# ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ
save_pdf_metadata()
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒœ์Šคํฌ๋กœ ์บ์‹ฑ ์‹คํ–‰
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/permanent-pdf-projects")
async def get_permanent_pdf_projects_api():
pdf_files = get_permanent_pdf_files()
projects_data = []
for pdf_file in pdf_files:
# PDF ID ์ฐพ๊ธฐ
pdf_id = None
for pid, path in pdf_metadata.items():
if os.path.basename(path) == pdf_file.name:
pdf_id = pid
break
# ID๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
if not pdf_id:
pdf_id = generate_pdf_id(pdf_file.name)
pdf_metadata[pdf_id] = str(pdf_file)
save_pdf_metadata()
projects_data.append({
"path": str(pdf_file),
"name": pdf_file.stem,
"id": pdf_id,
"cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
})
return projects_data
# API ์—”๋“œํฌ์ธํŠธ: PDF ID๋กœ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
@app.get("/api/pdf-info-by-id/{pdf_id}")
async def get_pdf_info_by_id(pdf_id: str):
pdf_path = get_pdf_path_by_id(pdf_id)
if pdf_path:
pdf_file = pathlib.Path(pdf_path)
return {
"path": pdf_path,
"name": pdf_file.stem,
"id": pdf_id,
"exists": True,
"cached": pdf_file.stem in pdf_cache and pdf_cache[pdf_file.stem].get("status") == "completed"
}
return {"exists": False, "error": "PDF๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}
# 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("pages"):
if 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.2, 0.2)) # ๋” ์ž‘์€ ์ธ๋„ค์ผ
img_data = pix.tobytes("jpeg", 70) # JPEG ์••์ถ• ์‚ฌ์šฉ
b64_img = base64.b64encode(img_data).decode('utf-8')
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
asyncio.create_task(cache_pdf(path))
return {"thumbnail": f"data:image/jpeg;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.post("/api/ai/query-pdf/{pdf_id}")
async def api_query_pdf(pdf_id: str, query: Dict[str, str]):
try:
user_query = query.get("query", "")
if not user_query:
return JSONResponse(content={"error": "์งˆ๋ฌธ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค"}, status_code=400)
# PDF ๊ฒฝ๋กœ ํ™•์ธ
pdf_path = get_pdf_path_by_id(pdf_id)
if not pdf_path:
return JSONResponse(content={"error": f"PDF ID {pdf_id}์— ํ•ด๋‹นํ•˜๋Š” ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}, status_code=404)
# ์งˆ์˜์‘๋‹ต ์ฒ˜๋ฆฌ
result = await query_pdf(pdf_id, user_query)
if "error" in result:
return JSONResponse(content={"error": result["error"]}, status_code=500)
return result
except Exception as e:
logger.error(f"์งˆ์˜์‘๋‹ต API ์˜ค๋ฅ˜: {e}")
return JSONResponse(content={"error": str(e)}, status_code=500)
# API ์—”๋“œํฌ์ธํŠธ: PDF ์š”์•ฝ
@app.get("/api/ai/summarize-pdf/{pdf_id}")
async def api_summarize_pdf(pdf_id: str):
try:
# PDF ๊ฒฝ๋กœ ํ™•์ธ
pdf_path = get_pdf_path_by_id(pdf_id)
if not pdf_path:
return JSONResponse(content={"error": f"PDF ID {pdf_id}์— ํ•ด๋‹นํ•˜๋Š” ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}, status_code=404)
# ์š”์•ฝ ์ฒ˜๋ฆฌ
result = await summarize_pdf(pdf_id)
if "error" in result:
return JSONResponse(content={"error": result["error"]}, status_code=500)
return result
except Exception as e:
logger.error(f"PDF ์š”์•ฝ API ์˜ค๋ฅ˜: {e}")
return JSONResponse(content={"error": str(e)}, status_code=500)
# 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:
status = pdf_cache[pdf_name].get("status", "")
# ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
if status == "completed":
return pdf_cache[pdf_name]
# ์ฒ˜๋ฆฌ ์ค‘์ธ ๊ฒฝ์šฐ ํ˜„์žฌ๊นŒ์ง€์˜ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ํฌํ•จ (์ ์ง„์  ๋กœ๋”ฉ)
elif status == "processing":
progress = pdf_cache[pdf_name].get("progress", 0)
pages = pdf_cache[pdf_name].get("pages", [])
total_pages = pdf_cache[pdf_name].get("total_pages", 0)
# ์ผ๋ถ€๋งŒ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ์—๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํŽ˜์ด์ง€ ์ œ๊ณต
return {
"status": "processing",
"progress": progress,
"pages": pages,
"total_pages": total_pages,
"available_pages": len([p for p in pages if p and p.get("src")])
}
# ์บ์‹œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
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"
or (pdf_cache[pdf_name].get("status") == "processing"
and pdf_cache[pdf_name].get("progress", 0) > 10)):
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)
# PDF ์—…๋กœ๋“œ ์—”๋“œํฌ์ธํŠธ - ์˜๊ตฌ ์ €์žฅ์†Œ์— ์ €์žฅ ๋ฐ ๋ฉ”์ธ ํ™”๋ฉด์— ์ž๋™ ํ‘œ์‹œ
@app.post("/api/upload-pdf")
async def upload_pdf(file: UploadFile = File(...)):
try:
# ํŒŒ์ผ ์ด๋ฆ„ ํ™•์ธ
if not file.filename.lower().endswith('.pdf'):
return JSONResponse(
content={"success": False, "message": "PDF ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค"},
status_code=400
)
# ์˜๊ตฌ ์ €์žฅ์†Œ์— ํŒŒ์ผ ์ €์žฅ
file_path = PERMANENT_PDF_DIR / file.filename
# ํŒŒ์ผ ์ฝ๊ธฐ ๋ฐ ์ €์žฅ
content = await file.read()
with open(file_path, "wb") as buffer:
buffer.write(content)
# ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์—๋„ ์ž๋™์œผ๋กœ ๋ณต์‚ฌ (์ž๋™ ํ‘œ์‹œ)
with open(PDF_DIR / file.filename, "wb") as buffer:
buffer.write(content)
# PDF ID ์ƒ์„ฑ ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
pdf_id = generate_pdf_id(file.filename)
pdf_metadata[pdf_id] = str(file_path)
save_pdf_metadata()
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
asyncio.create_task(cache_pdf(str(file_path)))
return JSONResponse(
content={
"success": True,
"path": str(file_path),
"name": file_path.stem,
"id": pdf_id,
"viewUrl": f"/view/{pdf_id}"
},
status_code=200
)
except Exception as e:
import traceback
error_details = traceback.format_exc()
logger.error(f"PDF ์—…๋กœ๋“œ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
return JSONResponse(
content={"success": False, "message": str(e)},
status_code=500
)
# ํ…์ŠคํŠธ ํŒŒ์ผ์„ PDF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
async def convert_text_to_pdf(text_content: str, title: str) -> str:
try:
# ์ œ๋ชฉ์—์„œ ์œ ํšจํ•œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ
import re
safe_title = re.sub(r'[^\w\-_\. ]', '_', title)
if not safe_title:
safe_title = "aibook"
# ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€๋กœ ๊ณ ์œ ํ•œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ
timestamp = int(time.time())
filename = f"{safe_title}_{timestamp}.pdf"
# ์˜๊ตฌ ์ €์žฅ์†Œ์˜ ํŒŒ์ผ ๊ฒฝ๋กœ
file_path = PERMANENT_PDF_DIR / filename
# ๊ธฐ๋ณธ Helvetica ํฐํŠธ ์‚ฌ์šฉ (ํ•œ๊ธ€ ์ง€์› ์—†์Œ)
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
# PDF ์ƒ์„ฑ
c = canvas.Canvas(str(file_path), pagesize=letter)
# ํŽ˜์ด์ง€ ํฌ๊ธฐ ์„ค์ •
page_width, page_height = letter
# ์ œ๋ชฉ ์ถ”๊ฐ€
c.setFont("Helvetica-Bold", 16)
c.drawCentredString(page_width/2, page_height - 50, title)
# ๋ณธ๋ฌธ ํ…์ŠคํŠธ ์ถ”๊ฐ€
c.setFont("Helvetica", 11)
y_position = page_height - 100
line_height = 14
# ํ…์ŠคํŠธ๋ฅผ ๋‹จ๋ฝ์œผ๋กœ ๋ถ„๋ฆฌ
paragraphs = text_content.split('\n\n')
for para in paragraphs:
if not para.strip():
continue
# ๋‹จ๋ฝ ๋‚ด ์ค„ ๋ฐ”๊ฟˆ ์ฒ˜๋ฆฌ
lines = para.split('\n')
for line in lines:
# ํ•œ ์ค„์˜ ์ตœ๋Œ€ ๋ฌธ์ž ์ˆ˜
max_chars_per_line = 80
# ๊ธด ์ค„ ๊ฐ์‹ธ๊ธฐ
import textwrap
wrapped_lines = textwrap.wrap(line, width=max_chars_per_line)
for wrapped_line in wrapped_lines:
# ํŽ˜์ด์ง€ ๋ฐ”๊ฟˆ ํ™•์ธ
if y_position < 50:
c.showPage()
c.setFont("Helvetica", 11)
y_position = page_height - 50
try:
# ASCII ๋ฌธ์ž๋งŒ ์ฒ˜๋ฆฌ
ascii_line = ''.join(c if ord(c) < 128 else ' ' for c in wrapped_line)
c.drawString(50, y_position, ascii_line)
except:
# ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๊ณต๋ฐฑ์œผ๋กœ ๋Œ€์ฒด
c.drawString(50, y_position, "[ํ…์ŠคํŠธ ๋ณ€ํ™˜ ์˜ค๋ฅ˜]")
y_position -= line_height
# ๋‹จ๋ฝ ๊ฐ„ ๊ฐ„๊ฒฉ
y_position -= 10
# PDF ์ €์žฅ
c.save()
# ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์—๋„ ๋ณต์‚ฌ
shutil.copy2(file_path, PDF_DIR / filename)
# PDF ID ์ƒ์„ฑ ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
pdf_id = generate_pdf_id(filename)
pdf_metadata[pdf_id] = str(file_path)
save_pdf_metadata()
# ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์บ์‹ฑ ์‹œ์ž‘
asyncio.create_task(cache_pdf(str(file_path)))
return {
"path": str(file_path),
"filename": filename,
"id": pdf_id
}
except Exception as e:
logger.error(f"ํ…์ŠคํŠธ๋ฅผ PDF๋กœ ๋ณ€ํ™˜ ์ค‘ ์˜ค๋ฅ˜: {e}")
raise e
# AI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…์ŠคํŠธ๋ฅผ ๋” ๊ตฌ์กฐํ™”๋œ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ (OpenAI ์ œ๊ฑฐ ๋ฒ„์ „)
async def enhance_text_with_ai(text_content: str, title: str) -> str:
# ์›๋ณธ ํ…์ŠคํŠธ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ (AI ํ–ฅ์ƒ ๊ธฐ๋Šฅ ๋น„ํ™œ์„ฑํ™”)
return text_content
# ํ…์ŠคํŠธ ํŒŒ์ผ์„ PDF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ
@app.post("/api/text-to-pdf")
async def text_to_pdf(file: UploadFile = File(...)):
try:
# ์ง€์›ํ•˜๋Š” ํŒŒ์ผ ํ˜•์‹ ํ™•์ธ
filename = file.filename.lower()
if not (filename.endswith('.txt') or filename.endswith('.docx') or filename.endswith('.doc')):
return JSONResponse(
content={"success": False, "message": "์ง€์›ํ•˜๋Š” ํŒŒ์ผ ํ˜•์‹์€ .txt, .docx, .doc์ž…๋‹ˆ๋‹ค."},
status_code=400
)
# ํŒŒ์ผ ๋‚ด์šฉ ์ฝ๊ธฐ
content = await file.read()
# ํŒŒ์ผ ํƒ€์ž…์— ๋”ฐ๋ผ ํ…์ŠคํŠธ ์ถ”์ถœ
if filename.endswith('.txt'):
# ์ธ์ฝ”๋”ฉ ์ž๋™ ๊ฐ์ง€ ์‹œ๋„
encodings = ['utf-8', 'euc-kr', 'cp949', 'latin1']
text_content = None
for encoding in encodings:
try:
text_content = content.decode(encoding, errors='strict')
logger.info(f"ํ…์ŠคํŠธ ํŒŒ์ผ ์ธ์ฝ”๋”ฉ ๊ฐ์ง€: {encoding}")
break
except UnicodeDecodeError:
continue
if text_content is None:
# ๋ชจ๋“  ์ธ์ฝ”๋”ฉ ์‹œ๋„ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ์ ์œผ๋กœ UTF-8๋กœ ์‹œ๋„ํ•˜๊ณ  ์˜ค๋ฅ˜๋Š” ๋Œ€์ฒด ๋ฌธ์ž๋กœ ์ฒ˜๋ฆฌ
text_content = content.decode('utf-8', errors='replace')
logger.warning("ํ…์ŠคํŠธ ํŒŒ์ผ ์ธ์ฝ”๋”ฉ์„ ๊ฐ์ง€ํ•  ์ˆ˜ ์—†์–ด UTF-8์œผ๋กœ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.")
elif filename.endswith('.docx') or filename.endswith('.doc'):
# ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(filename)[1]) as temp_file:
temp_file.write(content)
temp_path = temp_file.name
try:
# docx2txt๋กœ ํ…์ŠคํŠธ ์ถ”์ถœ
text_content = docx2txt.process(temp_path)
finally:
# ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
os.unlink(temp_path)
# ํŒŒ์ผ๋ช…์—์„œ ์ œ๋ชฉ ์ถ”์ถœ (ํ™•์žฅ์ž ์ œ์™ธ)
title = os.path.splitext(filename)[0]
# AI๋กœ ํ…์ŠคํŠธ ๋‚ด์šฉ ํ–ฅ์ƒ
enhanced_text = await enhance_text_with_ai(text_content, title)
# ํ…์ŠคํŠธ๋ฅผ PDF๋กœ ๋ณ€ํ™˜
pdf_info = await convert_text_to_pdf(enhanced_text, title)
return JSONResponse(
content={
"success": True,
"path": pdf_info["path"],
"name": os.path.splitext(pdf_info["filename"])[0],
"id": pdf_info["id"],
"viewUrl": f"/view/{pdf_info['id']}"
},
status_code=200
)
except Exception as e:
import traceback
error_details = traceback.format_exc()
logger.error(f"ํ…์ŠคํŠธ๋ฅผ PDF๋กœ ๋ณ€ํ™˜ ์ค‘ ์˜ค๋ฅ˜: {str(e)}\n{error_details}")
return JSONResponse(
content={"success": False, "message": str(e)},
status_code=500
)
# ๊ด€๋ฆฌ์ž ์ธ์ฆ ์—”๋“œํฌ์ธํŠธ
@app.post("/api/admin-login")
async def admin_login(password: str = Form(...)):
if password == ADMIN_PASSWORD:
return {"success": True}
return {"success": False, "message": "์ธ์ฆ ์‹คํŒจ"}
# ๊ด€๋ฆฌ์ž์šฉ PDF ์‚ญ์ œ ์—”๋“œํฌ์ธํŠธ
@app.delete("/api/admin/delete-pdf")
async def delete_pdf(path: str):
try:
pdf_file = pathlib.Path(path)
if not pdf_file.exists():
return {"success": False, "message": "ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}
# PDF ํŒŒ์ผ๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ
filename = pdf_file.name
# PDF ํŒŒ์ผ ์‚ญ์ œ (์˜๊ตฌ ์ €์žฅ์†Œ์—์„œ)
pdf_file.unlink()
# ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ๋„ ๋™์ผํ•œ ํŒŒ์ผ์ด ์žˆ์œผ๋ฉด ์‚ญ์ œ (๋ฒ„๊ทธ ์ˆ˜์ •)
main_file_path = PDF_DIR / filename
if main_file_path.exists():
main_file_path.unlink()
# ๊ด€๋ จ ์บ์‹œ ํŒŒ์ผ ์‚ญ์ œ
pdf_name = pdf_file.stem
cache_path = get_cache_path(pdf_name)
if cache_path.exists():
cache_path.unlink()
# ์บ์‹œ ๋ฉ”๋ชจ๋ฆฌ์—์„œ๋„ ์ œ๊ฑฐ
if pdf_name in pdf_cache:
del pdf_cache[pdf_name]
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ํŒŒ์ผ ID ์ œ๊ฑฐ
to_remove = []
for pid, fpath in pdf_metadata.items():
if os.path.basename(fpath) == filename:
to_remove.append(pid)
for pid in to_remove:
del pdf_metadata[pid]
save_pdf_metadata()
return {"success": True}
except Exception as e:
logger.error(f"PDF ์‚ญ์ œ ์˜ค๋ฅ˜: {str(e)}")
return {"success": False, "message": str(e)}
# PDF๋ฅผ ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์— ํ‘œ์‹œ ์„ค์ •
@app.post("/api/admin/feature-pdf")
async def feature_pdf(path: str):
try:
pdf_file = pathlib.Path(path)
if not pdf_file.exists():
return {"success": False, "message": "ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}
# ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์— ๋ณต์‚ฌ
target_path = PDF_DIR / pdf_file.name
shutil.copy2(pdf_file, target_path)
return {"success": True}
except Exception as e:
logger.error(f"PDF ํ‘œ์‹œ ์„ค์ • ์˜ค๋ฅ˜: {str(e)}")
return {"success": False, "message": str(e)}
# PDF๋ฅผ ๋ฉ”์ธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ์ œ๊ฑฐ (์˜๊ตฌ ์ €์žฅ์†Œ์—์„œ๋Š” ์œ ์ง€)
@app.delete("/api/admin/unfeature-pdf")
async def unfeature_pdf(path: str):
try:
pdf_name = pathlib.Path(path).name
target_path = PDF_DIR / pdf_name
if target_path.exists():
target_path.unlink()
return {"success": True}
except Exception as e:
logger.error(f"PDF ํ‘œ์‹œ ํ•ด์ œ ์˜ค๋ฅ˜: {str(e)}")
return {"success": False, "message": str(e)}
# ์ง์ ‘ PDF ๋ทฐ์–ด URL ์ ‘๊ทผ์šฉ ๋ผ์šฐํŠธ
@app.get("/view/{pdf_id}")
async def view_pdf_by_id(pdf_id: str):
# PDF ID ์œ ํšจํ•œ์ง€ ํ™•์ธ
pdf_path = get_pdf_path_by_id(pdf_id)
if not pdf_path:
# ์ผ๋‹จ ๋ชจ๋“  PDF ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•˜๊ณ  ์žฌ์‹œ๋„
load_pdf_metadata()
pdf_path = get_pdf_path_by_id(pdf_id)
if not pdf_path:
# ๋ชจ๋“  PDF ํŒŒ์ผ์„ ์ง์ ‘ ์Šค์บ”ํ•˜์—ฌ ์œ ์‚ฌํ•œ ์ด๋ฆ„ ์ฐพ๊ธฐ
for file_path in get_pdf_files() + get_permanent_pdf_files():
name_part = pdf_id.split('_')[0] if '_' in pdf_id else pdf_id
if file_path.stem.startswith(name_part):
pdf_metadata[pdf_id] = str(file_path)
save_pdf_metadata()
pdf_path = str(file_path)
break
if not pdf_path:
return HTMLResponse(
content=f"<html><body><h1>PDF๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</h1><p>ID: {pdf_id}</p><a href='/'>ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a></body></html>",
status_code=404
)
# ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๋˜, PDF ID ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
return get_html_content(pdf_id=pdf_id)
# HTML ํŒŒ์ผ ์ฝ๊ธฐ ํ•จ์ˆ˜
def get_html_content(pdf_id: str = None):
html_path = BASE / "flipbook_template.html"
content = ""
if html_path.exists():
with open(html_path, "r", encoding="utf-8") as f:
content = f.read()
else:
content = HTML # ๊ธฐ๋ณธ HTML ์‚ฌ์šฉ
# PDF ID๊ฐ€ ์ œ๊ณต๋œ ๊ฒฝ์šฐ, ์ž๋™ ๋กœ๋“œ ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€
if pdf_id:
auto_load_script = f"""
<script>
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ž๋™์œผ๋กœ ํ•ด๋‹น PDF ์—ด๊ธฐ
document.addEventListener('DOMContentLoaded', async function() {{
try {{
// PDF ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
const response = await fetch('/api/pdf-info-by-id/{pdf_id}');
const pdfInfo = await response.json();
if (pdfInfo.exists && pdfInfo.path) {{
// ์•ฝ๊ฐ„์˜ ์ง€์—ฐ ํ›„ PDF ๋ทฐ์–ด ์—ด๊ธฐ (UI๊ฐ€ ์ค€๋น„๋œ ํ›„)
setTimeout(() => {{
openPdfById('{pdf_id}', pdfInfo.path, pdfInfo.cached);
}}, 500);
}} else {{
showError("์š”์ฒญํ•œ PDF๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}}
}} catch (e) {{
console.error("์ž๋™ PDF ๋กœ๋“œ ์˜ค๋ฅ˜:", e);
}}
}});
</script>
"""
# body ์ข…๋ฃŒ ํƒœ๊ทธ ์ „์— ์Šคํฌ๋ฆฝํŠธ ์‚ฝ์ž…
content = content.replace("</body>", auto_load_script + "</body>")
return HTMLResponse(content=content)
@app.get("/", response_class=HTMLResponse)
async def root(request: Request, pdf_id: Optional[str] = Query(None)):
# PDF ID๊ฐ€ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ œ๊ณต๋œ ๊ฒฝ์šฐ /view/{pdf_id}๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
if pdf_id:
return RedirectResponse(url=f"/view/{pdf_id}")
return get_html_content()
# HTML ๋ฌธ์ž์—ด (AI ๋ฒ„ํŠผ ๋ฐ ์ฑ—๋ด‡ UI ์ถ”๊ฐ€)
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; /* ํŒŒ์Šคํ…” ํผํ”Œ */
--ai-color: #86e8ab; /* AI ๋ฒ„ํŠผ ์ƒ‰์ƒ */
--ai-hover: #65d68a; /* AI ํ˜ธ๋ฒ„ ์ƒ‰์ƒ */
--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(135deg, #2a0845 0%, #6441a5 50%, #c9a8ff 100%);
background-attachment: fixed;
}
/* ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ๋ฐฐ๊ฒฝ ๋ณ€๊ฒฝ */
.viewer-mode {
background-image: linear-gradient(135deg, #30154e 0%, #6b47ad 50%, #d5b8ff 100%) !important;
}
/* ํ—ค๋” ์ œ๋ชฉ ์ œ๊ฑฐ ๋ฐ Home ๋ฒ„ํŠผ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ */
.floating-home, .floating-ai {
position: fixed;
top: 20px;
left: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow: var(--shadow-md);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: var(--transition);
overflow: hidden;
}
.floating-ai {
top: 90px; /* Home ๋ฒ„ํŠผ ์•„๋ž˜์— ์œ„์น˜ */
background: rgba(134, 232, 171, 0.9); /* AI ๋ฒ„ํŠผ ์ƒ‰์ƒ */
}
.floating-home:hover, .floating-ai:hover {
transform: scale(1.05);
box-shadow: var(--shadow-lg);
}
.floating-home .icon, .floating-ai .icon {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
font-size: 22px;
color: var(--primary-color);
transition: var(--transition);
}
.floating-ai .icon {
color: white;
}
.floating-home:hover .icon {
color: #8bc5f8;
}
.floating-ai:hover .icon {
color: #ffffff;
}
.floating-home .title, .floating-ai .title {
position: absolute;
left: 70px;
background: rgba(255, 255, 255, 0.95);
padding: 8px 20px;
border-radius: 20px;
box-shadow: var(--shadow-sm);
font-weight: 600;
font-size: 14px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transform: translateX(-10px);
transition: all 0.3s ease;
}
.floating-home:hover .title, .floating-ai:hover .title {
opacity: 1;
transform: translateX(0);
}
/* ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
#adminButton {
position: fixed;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow: var(--shadow-md);
border-radius: 30px;
padding: 8px 20px;
display: flex;
align-items: center;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: var(--transition);
z-index: 9999;
}
#adminButton i {
margin-right: 8px;
color: var(--accent-color);
}
#adminButton:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
/* ๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
z-index: 10000;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: var(--radius-md);
padding: 30px;
width: 90%;
max-width: 400px;
box-shadow: var(--shadow-lg);
text-align: center;
}
.modal-content h2 {
margin-top: 0;
color: var(--accent-color);
margin-bottom: 20px;
}
.modal-content input {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 16px;
box-sizing: border-box;
}
.modal-content button {
padding: 10px 20px;
border: none;
border-radius: var(--radius-sm);
background: var(--accent-color);
color: white;
font-weight: 600;
cursor: pointer;
margin: 0 5px;
transition: var(--transition);
}
.modal-content button:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.modal-content #adminLoginClose {
background: #f1f3f5;
color: var(--text-color);
}
#home, #viewerPage, #adminPage {
padding-top: 100px;
max-width: 1200px;
margin: 0 auto;
padding-bottom: 60px;
padding-left: 30px;
padding-right: 30px;
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);
}
/* ๊ด€๋ฆฌ์ž ์นด๋“œ ์ถ”๊ฐ€ ์Šคํƒ€์ผ */
.admin-card {
height: 300px;
}
.admin-card .card-inner {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.delete-btn, .feature-btn, .unfeature-btn {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
background: #ff7675;
color: white;
border: none;
border-radius: 20px;
padding: 5px 15px;
font-size: 12px;
cursor: pointer;
z-index: 10;
transition: var(--transition);
}
.feature-btn {
bottom: 95px;
background: #74b9ff;
}
.unfeature-btn {
bottom: 95px;
background: #a29bfe;
}
.delete-btn:hover, .feature-btn:hover, .unfeature-btn:hover {
opacity: 0.9;
transform: translateX(-50%) scale(1.05);
}
/* ๋ทฐ์–ด ์Šคํƒ€์ผ */
#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;
}
/* ํ—ค๋” ๋กœ๊ณ  ๋ฐ ํƒ€์ดํ‹€ */
.library-header {
position: fixed;
top: 12px;
left: 0;
right: 0;
text-align: center;
z-index: 100;
pointer-events: none;
}
.library-header .title {
display: inline-block;
padding: 8px 24px; /* ํŒจ๋”ฉ ์ถ•์†Œ */
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
border-radius: 25px; /* ํ…Œ๋‘๋ฆฌ ๋ชจ์„œ๋ฆฌ ์ถ•์†Œ */
box-shadow: var(--shadow-md);
font-size: 1.25rem; /* ๊ธ€์ž ํฌ๊ธฐ ์ถ•์†Œ (1.5rem์—์„œ 1.25rem์œผ๋กœ) */
font-weight: 600;
background-image: linear-gradient(120deg, #8e74eb 0%, #9d66ff 100%); /* ์ œ๋ชฉ ์ƒ‰์ƒ๋„ ๋ฐ”ํƒ•ํ™”๋ฉด๊ณผ ์–ด์šธ๋ฆฌ๊ฒŒ ๋ณ€๊ฒฝ */
-webkit-background-clip: text;
background-clip: text;
color: transparent;
pointer-events: all;
}
/* ์ ์ง„์  ๋กœ๋”ฉ ํ‘œ์‹œ */
.loading-pages {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
padding: 10px 20px;
border-radius: 20px;
box-shadow: var(--shadow-md);
font-size: 14px;
color: var(--text-color);
z-index: 9998;
text-align: center;
}
/* ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์Šคํƒ€์ผ */
#adminPage {
color: white;
max-width: 1400px;
}
#adminPage h1 {
font-size: 2rem;
margin-bottom: 30px;
text-align: center;
background-image: linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
#adminBackButton {
position: absolute;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow: var(--shadow-md);
border: none;
border-radius: 30px;
padding: 8px 20px;
display: flex;
align-items: center;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: var(--transition);
}
#adminBackButton:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
/* ๊ด€๋ฆฌ์ž ๊ทธ๋ฆฌ๋“œ */
#adminGrid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
/* AI ์ฑ—๋ด‡ UI ์Šคํƒ€์ผ */
#aiChatContainer {
display: none;
position: fixed;
top: 0;
right: 0;
width: 400px;
height: 100%;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1);
z-index: 9999;
transition: all 0.3s ease;
transform: translateX(100%);
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
#aiChatContainer.active {
transform: translateX(0);
}
#aiChatHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
#aiChatHeader h3 {
margin: 0;
color: #333;
font-size: 18px;
display: flex;
align-items: center;
}
#aiChatHeader h3 i {
margin-right: 10px;
color: var(--ai-color);
}
#aiChatClose {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: #666;
transition: var(--transition);
}
#aiChatClose:hover {
color: #333;
transform: scale(1.1);
}
#aiChatMessages {
flex: 1;
overflow-y: auto;
padding: 10px 0;
margin-bottom: 15px;
}
.chat-message {
margin-bottom: 15px;
display: flex;
align-items: flex-start;
}
.chat-message.user {
flex-direction: row-reverse;
}
.chat-avatar {
width: 35px;
height: 35px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
flex-shrink: 0;
}
.chat-message.user .chat-avatar {
margin-right: 0;
margin-left: 10px;
background: var(--primary-color);
color: white;
}
.chat-message.ai .chat-avatar {
background: var(--ai-color);
color: white;
}
.chat-content {
background: #f1f1f1;
padding: 12px 15px;
border-radius: 18px;
max-width: 75%;
word-break: break-word;
position: relative;
font-size: 14px;
line-height: 1.4;
}
.chat-message.user .chat-content {
background: var(--primary-color);
color: white;
border-bottom-right-radius: 4px;
}
.chat-message.ai .chat-content {
background: #f1f1f1;
color: #333;
border-bottom-left-radius: 4px;
}
#aiChatForm {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1);
padding-top: 15px;
}
#aiChatInput {
flex: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 25px;
font-size: 14px;
outline: none;
transition: var(--transition);
}
#aiChatInput:focus {
border-color: var(--ai-color);
box-shadow: 0 0 0 2px rgba(134, 232, 171, 0.2);
}
#aiChatSubmit {
background: var(--ai-color);
border: none;
color: white;
width: 45px;
height: 45px;
border-radius: 50%;
margin-left: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: var(--transition);
}
#aiChatSubmit:hover {
background: var(--ai-hover);
transform: scale(1.05);
}
#aiChatSubmit:disabled {
background: #ccc;
cursor: not-allowed;
}
.typing-indicator {
display: flex;
align-items: center;
margin-top: 5px;
font-size: 12px;
color: #666;
}
.typing-indicator span {
height: 8px;
width: 8px;
background: var(--ai-color);
border-radius: 50%;
display: inline-block;
margin-right: 3px;
animation: typing 1s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0% { transform: translateY(0); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0); }
}
.chat-time {
font-size: 10px;
color: #999;
margin-top: 5px;
text-align: right;
}
/* ์ฝ”๋“œ ๋ธ”๋ก ์Šคํƒ€์ผ */
.chat-content pre {
background: rgba(0, 0, 0, 0.05);
padding: 10px;
border-radius: 5px;
overflow-x: auto;
font-family: monospace;
font-size: 12px;
margin: 10px 0;
}
/* ๋งˆํฌ๋‹ค์šด ์Šคํƒ€์ผ */
.chat-content strong {
font-weight: bold;
}
.chat-content em {
font-style: italic;
}
.chat-content ul, .chat-content ol {
margin-left: 20px;
margin-top: 5px;
margin-bottom: 5px;
}
/* ๊ณต์œ  ๋ฒ„ํŠผ */
#shareChat {
padding: 8px 15px;
background: #f1f1f1;
border: none;
border-radius: 20px;
font-size: 12px;
color: #666;
cursor: pointer;
margin-top: 5px;
transition: var(--transition);
}
#shareChat:hover {
background: #ddd;
}
/* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
}
.card {
height: 240px;
}
.library-header .title {
font-size: 1.25rem;
padding: 10px 20px;
}
.floating-home, .floating-ai {
width: 50px;
height: 50px;
}
.floating-home .icon, .floating-ai .icon {
font-size: 18px;
}
#adminButton {
padding: 6px 15px;
font-size: 12px;
}
#aiChatContainer {
width: 100%;
}
}
</style>
</head>
<body>
<!-- ์ œ๋ชฉ์„ Home ๋ฒ„ํŠผ๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์–ด๋กœ ์ฒ˜๋ฆฌ -->
<div id="homeButton" class="floating-home" style="display:none;">
<div class="icon"><i class="fas fa-home"></i></div>
<div class="title">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</div>
</div>
<!-- AI ๋ฒ„ํŠผ ์ถ”๊ฐ€ -->
<div id="aiButton" class="floating-ai" style="display:none;">
<div class="icon"><i class="fas fa-robot"></i></div>
<div class="title">AI ์–ด์‹œ์Šคํ„ดํŠธ</div>
</div>
<!-- AI ์ฑ—๋ด‡ ์ปจํ…Œ์ด๋„ˆ -->
<div id="aiChatContainer">
<div id="aiChatHeader">
<h3><i class="fas fa-robot"></i> AI ์–ด์‹œ์Šคํ„ดํŠธ</h3>
<button id="aiChatClose"><i class="fas fa-times"></i></button>
</div>
<div id="aiChatMessages"></div>
<form id="aiChatForm">
<input type="text" id="aiChatInput" placeholder="PDF์— ๋Œ€ํ•ด ์งˆ๋ฌธํ•˜์„ธ์š”..." autocomplete="off">
<button type="submit" id="aiChatSubmit"><i class="fas fa-paper-plane"></i></button>
</form>
</div>
<!-- ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ -->
<div id="adminButton">
<i class="fas fa-cog"></i> Admin
</div>
<!-- ๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ -->
<div id="adminLoginModal" class="modal">
<div class="modal-content">
<h2>๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ</h2>
<input type="password" id="adminPasswordInput" placeholder="๊ด€๋ฆฌ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ">
<div>
<button id="adminLoginButton">๋กœ๊ทธ์ธ</button>
<button id="adminLoginClose">์ทจ์†Œ</button>
</div>
</div>
</div>
<!-- ์„ผํ„ฐ ์ •๋ ฌ๋œ ํƒ€์ดํ‹€ -->
<div class="library-header">
<div class="title">AI FlipBook Maker</div>
</div>
<section id="home" class="fade-in">
<div class="upload-container">
<button class="upload" id="pdfUploadBtn">
<i class="fas fa-file-pdf"></i> PDF Upload
</button>
<button class="upload" id="textToAIBookBtn">
<i class="fas fa-file-alt"></i> Text to AI-Book
</button>
<input id="pdfInput" type="file" accept="application/pdf" style="display:none">
<input id="textInput" type="file" accept=".txt,.docx,.doc" style="display:none">
</div>
<div class="section-title">Projects</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>
<div id="loadingPages" class="loading-pages" style="display:none;">ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0/0</span></div>
</section>
<!-- ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ -->
<section id="adminPage" style="display:none" class="fade-in">
<h1>๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€</h1>
<button id="adminBackButton"><i class="fas fa-arrow-left"></i> ๋’ค๋กœ ๊ฐ€๊ธฐ</button>
<div class="section-title">์ €์žฅ๋œ PDF ๋ชฉ๋ก</div>
<div class="grid" id="adminGrid">
<!-- ๊ด€๋ฆฌ์ž PDF ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
</div>
<div id="noAdminProjects" class="no-projects" style="display: none;">
์ €์žฅ๋œ PDF๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. PDF๋ฅผ ์—…๋กœ๋“œํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
</div>
</section>
<script>
let projects=[], fb=null;
const grid=document.getElementById('grid'), viewer=document.getElementById('viewer');
pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
// ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
let serverProjects = [];
// ํ˜„์žฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ
let currentLoadingPdfPath = null;
let pageLoadingInterval = null;
// ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID
let currentPdfId = null;
// ์˜ค๋””์˜ค ์ปจํ…์ŠคํŠธ์™€ ์ดˆ๊ธฐํ™” ์ƒํƒœ ๊ด€๋ฆฌ
let audioInitialized = false;
let audioContext = null;
// AI ์ฑ—๋ด‡ ๊ด€๋ จ ๋ณ€์ˆ˜
let isAiChatActive = false;
let isAiProcessing = false;
let hasLoadedSummary = false;
// ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
function initializeAudio() {
if (audioInitialized) return Promise.resolve();
return new Promise((resolve) => {
// ์˜ค๋””์˜ค ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ (์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์ด ํ•„์š” ์—†๋Š” ์ดˆ๊ธฐํ™”)
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// MP3 ๋กœ๋“œ ๋ฐ ์ดˆ๊ธฐํ™”
const audio = new Audio('/static/turnPage2.mp3');
audio.volume = 0.01; // ์ตœ์†Œ ๋ณผ๋ฅจ์œผ๋กœ ์„ค์ •
// ๋กœ๋“œ๋œ ์˜ค๋””์˜ค ์žฌ์ƒ ์‹œ๋„ (์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์š”๊ตฌ๋  ์ˆ˜ ์žˆ์Œ)
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise
.then(() => {
// ์„ฑ๊ณต์ ์œผ๋กœ ์žฌ์ƒ๋จ - ์ฆ‰์‹œ ์ผ์‹œ์ •์ง€
audio.pause();
audioInitialized = true;
console.log('์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์„ฑ๊ณต');
resolve();
})
.catch((error) => {
console.log('์ž๋™ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ, ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ํ•„์š”:', error);
// ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
const initOnUserAction = function() {
const tempAudio = new Audio('/static/turnPage2.mp3');
tempAudio.volume = 0.01;
tempAudio.play()
.then(() => {
tempAudio.pause();
audioInitialized = true;
console.log('์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์œผ๋กœ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์„ฑ๊ณต');
resolve();
// ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ
['click', 'touchstart', 'keydown'].forEach(event => {
document.removeEventListener(event, initOnUserAction, { capture: true });
});
})
.catch(e => console.error('์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ:', e));
};
// ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์ด๋ฒคํŠธ์— ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
['click', 'touchstart', 'keydown'].forEach(event => {
document.addEventListener(event, initOnUserAction, { once: true, capture: true });
});
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์งํ›„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ค๋””์˜ค ํ™œ์„ฑํ™” ์š”์ฒญ
if (window.location.pathname.startsWith('/view/')) {
// ์˜ค๋””์˜ค ํ™œ์„ฑํ™” ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ (๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ๋กœ ์ ‘์†ํ•œ ๊ฒฝ์šฐ)
setTimeout(() => {
const audioPrompt = document.createElement('div');
audioPrompt.style.position = 'fixed';
audioPrompt.style.bottom = '80px';
audioPrompt.style.left = '50%';
audioPrompt.style.transform = 'translateX(-50%)';
audioPrompt.style.backgroundColor = 'rgba(0,0,0,0.7)';
audioPrompt.style.color = 'white';
audioPrompt.style.padding = '10px 20px';
audioPrompt.style.borderRadius = '20px';
audioPrompt.style.zIndex = '10000';
audioPrompt.style.cursor = 'pointer';
audioPrompt.innerHTML = 'ํŽ˜์ด์ง€ ์–ด๋””๋“  ํด๋ฆญํ•˜์—ฌ ์†Œ๋ฆฌ ํšจ๊ณผ ํ™œ์„ฑํ™” <i class="fas fa-volume-up"></i>';
audioPrompt.id = 'audioPrompt';
// ํด๋ฆญ ์‹œ ์†Œ๋ฆฌ ํ™œ์„ฑํ™” ๋ฐ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
audioPrompt.addEventListener('click', function() {
initOnUserAction();
audioPrompt.remove();
});
document.body.appendChild(audioPrompt);
// 10์ดˆ ํ›„ ์ž๋™์œผ๋กœ ์ˆจ๊น€
setTimeout(() => {
if (document.getElementById('audioPrompt')) {
document.getElementById('audioPrompt').remove();
}
}, 10000);
}, 2000);
}
});
} else {
// ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Promise ๊ธฐ๋ฐ˜ ์žฌ์ƒ์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ
audioInitialized = true;
resolve();
}
});
}
/* โ”€โ”€ ์œ ํ‹ธ โ”€โ”€ */
function $id(id){return document.getElementById(id)}
// ํ˜„์žฌ ์‹œ๊ฐ„์„ ํฌ๋งทํŒ…ํ•˜๋Š” ํ•จ์ˆ˜
function formatTime() {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
// AI ์ฑ—๋ด‡ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
function addChatMessage(content, isUser = false) {
const messagesContainer = $id('aiChatMessages');
const messageElement = document.createElement('div');
messageElement.className = `chat-message ${isUser ? 'user' : 'ai'}`;
const currentTime = formatTime();
messageElement.innerHTML = `
<div class="chat-avatar">
<i class="fas ${isUser ? 'fa-user' : 'fa-robot'}"></i>
</div>
<div class="chat-bubble">
<div class="chat-content">${content}</div>
<div class="chat-time">${currentTime}</div>
</div>
`;
messagesContainer.appendChild(messageElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return messageElement;
}
// ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ์ถ”๊ฐ€ ํ•จ์ˆ˜
function addTypingIndicator() {
const messagesContainer = $id('aiChatMessages');
const indicatorElement = document.createElement('div');
indicatorElement.className = 'typing-indicator';
indicatorElement.innerHTML = `
<div class="chat-avatar">
<i class="fas fa-robot"></i>
</div>
<div>
<span></span>
<span></span>
<span></span>
</div>
`;
messagesContainer.appendChild(indicatorElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return indicatorElement;
}
// AI ์ฑ—๋ด‡ ํ† ๊ธ€ ํ•จ์ˆ˜
function toggleAiChat(show = true) {
const aiChatContainer = $id('aiChatContainer');
if (show) {
// ์ฑ—๋ด‡ ํ‘œ์‹œ
aiChatContainer.style.display = 'flex';
setTimeout(() => {
aiChatContainer.classList.add('active');
}, 10);
isAiChatActive = true;
// ์ฒ˜์Œ ์—ด ๋•Œ ์ž๋™ ์š”์•ฝ ๋กœ๋“œ
if (!hasLoadedSummary && currentPdfId) {
loadPdfSummary();
}
} else {
// ์ฑ—๋ด‡ ์ˆจ๊ธฐ๊ธฐ
aiChatContainer.classList.remove('active');
setTimeout(() => {
aiChatContainer.style.display = 'none';
}, 300);
isAiChatActive = false;
}
}
// PDF ์š”์•ฝ ๋กœ๋“œ ํ•จ์ˆ˜
// PDF ์š”์•ฝ ๋กœ๋“œ ํ•จ์ˆ˜
async function loadPdfSummary() {
if (!currentPdfId || isAiProcessing || hasLoadedSummary) return;
try {
isAiProcessing = true;
const typingIndicator = addTypingIndicator();
// ์„œ๋ฒ„์— ์š”์•ฝ ์š”์ฒญ
const response = await fetch(`/api/ai/summarize-pdf/${currentPdfId}`);
const data = await response.json();
// ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ์ œ๊ฑฐ
typingIndicator.remove();
if (data.error) {
// ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
addChatMessage(`PDF ์š”์•ฝ์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${data.error}<br><br>๊ณ„์† ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์‹œ๋ฉด PDF ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ต๋ณ€์„ ์‹œ๋„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.`);
// ์š”์•ฝ์ด ์‹คํŒจํ•ด๋„ ํŠน์ • ๊ฒฝ์šฐ์—๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฌ๊ณ  ๊ณ„์† ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ •
if (data.summary) {
addChatMessage(`<strong>PDF์—์„œ ์ถ”์ถœํ•œ ์ •๋ณด:</strong><br>${data.summary}`);
hasLoadedSummary = true;
}
} else {
// ํ™˜์˜ ๋ฉ”์‹œ์ง€์™€ ์š”์•ฝ ์ถ”๊ฐ€
addChatMessage(`์•ˆ๋…•ํ•˜์„ธ์š”! ์ด PDF์— ๋Œ€ํ•ด ์–ด๋–ค ๊ฒƒ์ด๋“  ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”. ์ œ๊ฐ€ ๋„์™€๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.<br><br><strong>PDF ์š”์•ฝ:</strong><br>${data.summary}`);
hasLoadedSummary = true;
}
} catch (error) {
console.error("PDF ์š”์•ฝ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
addChatMessage(`PDF ์š”์•ฝ์„ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.<br><br>์–ด๋–ค ์งˆ๋ฌธ์ด๋“  ์ž…๋ ฅํ•˜์‹œ๋ฉด ์ตœ์„ ์„ ๋‹คํ•ด ๋‹ต๋ณ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.`);
} finally {
isAiProcessing = false;
}
}
// ์งˆ๋ฌธ ์ œ์ถœ ํ•จ์ˆ˜
async function submitQuestion(question) {
if (!currentPdfId || isAiProcessing || !question.trim()) return;
try {
isAiProcessing = true;
$id('aiChatSubmit').disabled = true;
// ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
addChatMessage(question, true);
// ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ์ถ”๊ฐ€
const typingIndicator = addTypingIndicator();
// ์„œ๋ฒ„์— ์งˆ์˜ ์š”์ฒญ
const response = await fetch(`/api/ai/query-pdf/${currentPdfId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query: question }),
// ํƒ€์ž„์•„์›ƒ ์„ค์ • ์ถ”๊ฐ€
signal: AbortSignal.timeout(60000) // 60์ดˆ ํƒ€์ž„์•„์›ƒ
});
const data = await response.json();
// ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ์ œ๊ฑฐ
typingIndicator.remove();
if (data.error) {
// ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์นœ์ ˆํ•œ ์•ˆ๋‚ด ์ œ๊ณต
if (data.error.includes("API ํ‚ค")) {
addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ AI ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž์—๊ฒŒ API ํ‚ค ์„ค์ •์„ ํ™•์ธํ•ด๋‹ฌ๋ผ๊ณ  ์š”์ฒญํ•ด์ฃผ์„ธ์š”.");
} else if (data.error.includes("์—ฐ๊ฒฐ")) {
addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. AI ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
} else {
addChatMessage(`์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${data.error}`);
}
} else {
// AI ์‘๋‹ต ์ถ”๊ฐ€
addChatMessage(data.answer);
}
} catch (error) {
console.error("์งˆ๋ฌธ ์ œ์ถœ ์˜ค๋ฅ˜:", error);
if (error.name === 'AbortError') {
addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์‘๋‹ต ์‹œ๊ฐ„์ด ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ ค ์š”์ฒญ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ๋” ์งง์€ ์งˆ๋ฌธ์œผ๋กœ ๋‹ค์‹œ ์‹œ๋„ํ•ด๋ณด์„ธ์š”.");
} else {
addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์™€ ํ†ต์‹  ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
}
} finally {
isAiProcessing = false;
$id('aiChatSubmit').disabled = false;
$id('aiChatInput').value = '';
$id('aiChatInput').focus();
}
}
// DOM์ด ๋กœ๋“œ๋˜๋ฉด ์‹คํ–‰
document.addEventListener('DOMContentLoaded', function() {
console.log("DOM ๋กœ๋“œ ์™„๋ฃŒ, ์ด๋ฒคํŠธ ์„ค์ • ์‹œ์ž‘");
// ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹œ๋„
initializeAudio().catch(e => console.warn('์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ:', e));
// PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ
const pdfBtn = document.getElementById('pdfUploadBtn');
const pdfInput = document.getElementById('pdfInput');
if (pdfBtn && pdfInput) {
console.log("PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ฐพ์Œ");
// ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํŒŒ์ผ ์ž…๋ ฅ ํŠธ๋ฆฌ๊ฑฐ
pdfBtn.addEventListener('click', function() {
console.log("PDF ๋ฒ„ํŠผ ํด๋ฆญ๋จ");
pdfInput.click();
});
// ํŒŒ์ผ ์„ ํƒ ์‹œ ์ฒ˜๋ฆฌ
pdfInput.addEventListener('change', function(e) {
console.log("PDF ํŒŒ์ผ ์„ ํƒ๋จ:", e.target.files.length);
const file = e.target.files[0];
if (!file) return;
// ์„œ๋ฒ„์— PDF ์—…๋กœ๋“œ (์˜๊ตฌ ์ €์žฅ์†Œ์— ์ €์žฅ)
uploadPdfToServer(file);
});
} else {
console.error("PDF ์—…๋กœ๋“œ ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ");
}
// ํ…์ŠคํŠธ ์—…๋กœ๋“œ ๋ฒ„ํŠผ
const textBtn = document.getElementById('textToAIBookBtn');
const textInput = document.getElementById('textInput');
if (textBtn && textInput) {
// ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํŒŒ์ผ ์ž…๋ ฅ ํŠธ๋ฆฌ๊ฑฐ
textBtn.addEventListener('click', function() {
textInput.click();
});
// ํŒŒ์ผ ์„ ํƒ ์‹œ ์ฒ˜๋ฆฌ
textInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
// ์„œ๋ฒ„์— ํ…์ŠคํŠธ ํŒŒ์ผ ์—…๋กœ๋“œ (์˜๊ตฌ ์ €์žฅ์†Œ์— PDF๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ)
uploadTextToServer(file);
});
}
// ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ
loadServerPDFs();
// ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
setInterval(checkCacheStatus, 3000);
// ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •
setupAdminFunctions();
// ํ™ˆ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •
const homeButton = document.getElementById('homeButton');
if (homeButton) {
homeButton.addEventListener('click', function() {
if(fb) {
fb.destroy();
viewer.innerHTML = '';
fb = null;
}
toggle(true);
// ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ •๋ฆฌ
if (pageLoadingInterval) {
clearInterval(pageLoadingInterval);
pageLoadingInterval = null;
}
$id('loadingPages').style.display = 'none';
currentLoadingPdfPath = null;
currentPdfId = null;
// AI ์ฑ—๋ด‡ ๋‹ซ๊ธฐ
toggleAiChat(false);
hasLoadedSummary = false; // ์š”์•ฝ ๋กœ๋“œ ์ƒํƒœ ์ดˆ๊ธฐํ™”
});
}
// AI ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •
const aiButton = document.getElementById('aiButton');
if (aiButton) {
aiButton.addEventListener('click', function() {
toggleAiChat(!isAiChatActive);
});
}
// AI ์ฑ—๋ด‡ ๋‹ซ๊ธฐ ๋ฒ„ํŠผ
const aiChatClose = document.getElementById('aiChatClose');
if (aiChatClose) {
aiChatClose.addEventListener('click', function() {
toggleAiChat(false);
});
}
// AI ์ฑ—๋ด‡ ํผ ์ œ์ถœ
const aiChatForm = document.getElementById('aiChatForm');
if (aiChatForm) {
aiChatForm.addEventListener('submit', function(e) {
e.preventDefault();
const inputField = document.getElementById('aiChatInput');
const question = inputField.value.trim();
if (question && !isAiProcessing) {
submitQuestion(question);
}
});
}
});
// ์„œ๋ฒ„์— PDF ์—…๋กœ๋“œ ํ•จ์ˆ˜
async function uploadPdfToServer(file) {
try {
showLoading("PDF ์—…๋กœ๋“œ ์ค‘...");
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload-pdf', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
hideLoading();
// ์—…๋กœ๋“œ ์„ฑ๊ณต ์‹œ ์„œ๋ฒ„ PDF ๋ฆฌ์ŠคํŠธ ๋ฆฌ๋กœ๋“œ
await loadServerPDFs();
// ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
showMessage("PDF๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๊ณต์œ  URL: " + result.viewUrl);
} else {
hideLoading();
showError("์—…๋กœ๋“œ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
}
} catch (error) {
console.error("PDF ์—…๋กœ๋“œ ์˜ค๋ฅ˜:", error);
hideLoading();
showError("PDF ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
}
// ์„œ๋ฒ„์— ํ…์ŠคํŠธ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์—ฌ PDF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
async function uploadTextToServer(file) {
try {
showLoading("ํ…์ŠคํŠธ ๋ถ„์„ ๋ฐ PDF ๋ณ€ํ™˜ ์ค‘...");
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/text-to-pdf', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
hideLoading();
// ์—…๋กœ๋“œ ์„ฑ๊ณต ์‹œ ์„œ๋ฒ„ PDF ๋ฆฌ์ŠคํŠธ ๋ฆฌ๋กœ๋“œ
await loadServerPDFs();
// ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
showMessage("ํ…์ŠคํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ PDF๋กœ ๋ณ€ํ™˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๊ณต์œ  URL: " + result.viewUrl);
} else {
hideLoading();
showError("๋ณ€ํ™˜ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
}
} catch (error) {
console.error("ํ…์ŠคํŠธ ๋ณ€ํ™˜ ์˜ค๋ฅ˜:", error);
hideLoading();
showError("ํ…์ŠคํŠธ๋ฅผ PDF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
}
function addCard(i, thumb, title, isCached = false, pdfId = null) {
const d = document.createElement('div');
d.className = 'card fade-in';
d.onclick = () => open(i);
// PDF ID๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ์ดํ„ฐ ์†์„ฑ์œผ๋กœ ์ €์žฅ
if (pdfId) {
d.dataset.pdfId = pdfId;
}
// ์ œ๋ชฉ ์ฒ˜๋ฆฌ
const displayTitle = title ?
(title.length > 15 ? title.substring(0, 15) + '...' : title) :
'ํ”„๋กœ์ ํŠธ ' + (i+1);
// ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์ถ”๊ฐ€
const cachedBadge = isCached ?
'<div class="cached-status">์บ์‹œ๋จ</div>' : '';
// ๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ ์ถ”๊ฐ€ (PDF ID๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ)
const linkHtml = pdfId ?
`<div style="position: absolute; bottom: 55px; left: 50%; transform: translateX(-50%); z-index:5;">
<a href="/view/${pdfId}" target="_blank" style="color:#4a6ee0; font-size:11px;">Share Link</a>
</div>` : '';
d.innerHTML = `
<div class="card-inner">
${cachedBadge}
<img src="${thumb}" alt="${displayTitle}" loading="lazy">
${linkHtml}
<p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
</div>
`;
grid.appendChild(d);
// ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์œผ๋ฉด 'ํ”„๋กœ์ ํŠธ ์—†์Œ' ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
$id('noProjects').style.display = 'none';
}
/* โ”€โ”€ ํ”„๋กœ์ ํŠธ ์ €์žฅ โ”€โ”€ */
function save(pages, title, isCached = false, pdfId = null) {
const id = projects.push(pages) - 1;
addCard(id, pages[0].thumb, title, isCached, pdfId);
}
/* โ”€โ”€ ์„œ๋ฒ„ 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();
// ๊ธฐ์กด ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™”
grid.innerHTML = '';
projects = [];
if (serverProjects.length === 0) {
hideLoading();
$id('noProjects').style.display = 'block';
return;
}
// ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์ธ๋„ค์ผ ์ƒ์„ฑ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์ตœ์ ํ™”)
const thumbnailPromises = serverProjects.map(async (project, index) => {
updateLoading(`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (${index+1}/${serverProjects.length})`);
const pdfName = project.name;
const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
try {
// ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
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
}];
return {
pages,
name: project.name,
isCached,
id: project.id
};
}
} catch (err) {
console.error(`์ธ๋„ค์ผ ๋กœ๋“œ ์˜ค๋ฅ˜ (${project.name}):`, err);
}
return null;
});
// ๋ชจ๋“  ์ธ๋„ค์ผ ์š”์ฒญ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ
const results = await Promise.all(thumbnailPromises);
// ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ ๊ฒฐ๊ณผ๋งŒ ํ‘œ์‹œ
results.filter(result => result !== null).forEach(result => {
save(result.pages, result.name, result.isCached, result.id);
});
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);
} else if (badgeEl.textContent !== '์บ์‹œ๋จ') {
badgeEl.textContent = '์บ์‹œ๋จ';
badgeEl.style.background = 'var(--accent-color)';
}
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)';
}
}
}
// ํ˜„์žฌ ๋กœ๋”ฉ ์ค‘์ธ PDF๊ฐ€ ์žˆ์œผ๋ฉด ์ƒํƒœ ํ™•์ธ
if (currentLoadingPdfPath && pageLoadingInterval) {
const pdfName = currentLoadingPdfPath.split('/').pop().replace('.pdf', '');
if (cacheStatus[pdfName]) {
const status = cacheStatus[pdfName].status;
const progress = cacheStatus[pdfName].progress || 0;
if (status === "completed") {
// ์บ์‹ฑ ์™„๋ฃŒ ์‹œ
clearInterval(pageLoadingInterval);
$id('loadingPages').style.display = 'none';
currentLoadingPdfPath = null;
// ์™„๋ฃŒ๋œ ์บ์‹œ๋กœ ํ”Œ๋ฆฝ๋ถ ๋‹ค์‹œ ๋กœ๋“œ
refreshFlipBook();
} else if (status === "processing") {
// ์ง„ํ–‰ ์ค‘์ผ ๋•Œ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
$id('loadingPages').style.display = 'block';
$id('loadingPagesCount').textContent = `${progress}%`;
}
}
}
} catch(error) {
console.error('์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
}
}
/* โ”€โ”€ PDF ID๋กœ PDF ์—ด๊ธฐ โ”€โ”€ */
async function openPdfById(pdfId, pdfPath, isCached = false) {
try {
// ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹œ๋„
await initializeAudio().catch(e => console.warn('PDF ์—ด๊ธฐ ์ „ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ:', e));
// ๋จผ์ € ํ™ˆ ํ™”๋ฉด์—์„œ ์นด๋“œ๋ฅผ ์ฐพ์•„์„œ ํด๋ฆญํ•˜๋Š” ๋ฐฉ๋ฒ• ์‹œ๋„
let foundCard = false;
const cards = document.querySelectorAll('.card');
for (let i = 0; i < cards.length; i++) {
if (cards[i].dataset.pdfId === pdfId) {
cards[i].click();
foundCard = true;
break;
}
}
// ์นด๋“œ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ ์ง์ ‘ ์˜คํ”ˆ
if (!foundCard) {
toggle(false);
showLoading("PDF ์ค€๋น„ ์ค‘...");
let pages = [];
// ์ด๋ฏธ ์บ์‹œ๋œ ๊ฒฝ์šฐ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
if (isCached) {
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);
// ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID ์ €์žฅ
currentPdfId = pdfId;
// AI ๋ฒ„ํŠผ ํ‘œ์‹œ
$id('aiButton').style.display = 'block';
return;
}
} catch (error) {
console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error);
}
}
// ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
try {
const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdfPath)}`);
const thumbData = await thumbResponse.json();
if (thumbData.thumbnail) {
pages = [{
src: thumbData.thumbnail,
thumb: thumbData.thumbnail,
path: pdfPath,
cached: isCached
}];
}
} catch (error) {
console.error("์ธ๋„ค์ผ ๋กœ๋“œ ์‹คํŒจ:", error);
}
// ์ผ๋‹จ ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ์ถ”๊ฐ€
if (pages.length === 0) {
pages = [{
path: pdfPath,
cached: isCached
}];
}
// ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ณ  ๋ทฐ์–ด ์‹คํ–‰
const projectId = projects.push(pages) - 1;
hideLoading();
open(projectId);
// ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID ์ €์žฅ
currentPdfId = pdfId;
// AI ๋ฒ„ํŠผ ํ‘œ์‹œ
$id('aiButton').style.display = 'block';
}
} catch (error) {
console.error("PDF ID๋กœ ์—ด๊ธฐ ์‹คํŒจ:", error);
hideLoading();
showError("PDF๋ฅผ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
}
}
/* โ”€โ”€ ํ˜„์žฌ PDF์˜ ๊ณ ์œ  URL ์ƒ์„ฑ ๋ฐ ๋ณต์‚ฌ โ”€โ”€ */
function copyPdfShareUrl() {
if (!currentPdfId) {
showError("๊ณต์œ ํ•  PDF๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
return;
}
// ํ˜„์žฌ ๋„๋ฉ”์ธ ๊ธฐ๋ฐ˜ ์ „์ฒด URL ์ƒ์„ฑ
const shareUrl = `${window.location.origin}/view/${currentPdfId}`;
// ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ
navigator.clipboard.writeText(shareUrl)
.then(() => {
showMessage("PDF ๋งํฌ๊ฐ€ ๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
})
.catch(err => {
console.error("ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ ์‹คํŒจ:", err);
showError("๋งํฌ ๋ณต์‚ฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
});
}
/* โ”€โ”€ ์นด๋“œ โ†’ FlipBook โ”€โ”€ */
async function open(i) {
toggle(false);
const pages = projects[i];
// PDF ID ์ฐพ๊ธฐ ๋ฐ ์ €์žฅ
const card = document.querySelectorAll('.card')[i];
if (card && card.dataset.pdfId) {
currentPdfId = card.dataset.pdfId;
// AI ๋ฒ„ํŠผ ํ‘œ์‹œ
$id('aiButton').style.display = 'block';
} else {
currentPdfId = null;
// AI ๋ฒ„ํŠผ ์ˆจ๊น€
$id('aiButton').style.display = 'none';
}
// AI ์ฑ—๋ด‡ ์ดˆ๊ธฐํ™”
toggleAiChat(false);
hasLoadedSummary = false;
$id('aiChatMessages').innerHTML = '';
// ๊ธฐ์กด FlipBook ์ •๋ฆฌ
if(fb) {
fb.destroy();
viewer.innerHTML = '';
}
// ์„œ๋ฒ„ PDF ๋˜๋Š” ๋กœ์ปฌ ํ”„๋กœ์ ํŠธ ์ฒ˜๋ฆฌ
if(pages[0].path) {
const pdfPath = pages[0].path;
// ์ ์ง„์  ๋กœ๋”ฉ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™”
let progressiveLoading = false;
currentLoadingPdfPath = pdfPath;
// ์บ์‹œ ์—ฌ๋ถ€ ํ™•์ธ
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);
currentLoadingPdfPath = null;
return;
} else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
// ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
hideLoading();
createFlipBook(cachedData.pages);
progressiveLoading = true;
// ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
}
} catch(error) {
console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
// ์บ์‹œ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์›๋ณธ PDF๋กœ ๋Œ€์ฒด
}
}
if (!progressiveLoading) {
// ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์„œ๋ฒ„ 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);
currentLoadingPdfPath = null;
return;
} else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
// ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
hideLoading();
createFlipBook(cachedData.pages);
// ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
return;
}
}
// ์›๋ณธ PDF ๋กœ๋“œ (ArrayBuffer ํ˜•ํƒœ)
const pdfResponse = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
// JSON ์‘๋‹ต์ธ ๊ฒฝ์šฐ (๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋“ฑ)
try {
const jsonData = await pdfResponse.clone().json();
if (jsonData.redirect) {
const redirectRes = await fetch(jsonData.redirect);
const cachedData = await redirectRes.json();
if(cachedData.pages && cachedData.pages.length > 0) {
hideLoading();
createFlipBook(cachedData.pages);
if(cachedData.status === "processing") {
startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
} else {
currentLoadingPdfPath = null;
}
return;
}
}
} catch (e) {
// JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์›๋ณธ PDF ๋ฐ์ดํ„ฐ๋กœ ์ฒ˜๋ฆฌ
}
// ArrayBuffer ํ˜•ํƒœ์˜ PDF ๋ฐ์ดํ„ฐ
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);
currentLoadingPdfPath = null;
} catch(error) {
console.error('PDF ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
hideLoading();
showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
currentLoadingPdfPath = null;
}
}
} else {
// ๋กœ์ปฌ ์—…๋กœ๋“œ๋œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰
createFlipBook(pages);
currentLoadingPdfPath = null;
}
}
/* โ”€โ”€ ์ ์ง„์  ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์‹œ์ž‘ โ”€โ”€ */
function startProgressiveLoadingIndicator(progress, totalPages) {
// ์ง„ํ–‰ ์ƒํƒœ ํ‘œ์‹œ ํ™œ์„ฑํ™”
$id('loadingPages').style.display = 'block';
$id('loadingPagesCount').textContent = `${progress}%`;
// ๊ธฐ์กด ์ธํ„ฐ๋ฒŒ ์ œ๊ฑฐ
if (pageLoadingInterval) {
clearInterval(pageLoadingInterval);
}
// ์ฃผ๊ธฐ์ ์œผ๋กœ ์บ์‹œ ์ƒํƒœ ํ™•์ธ (2์ดˆ๋งˆ๋‹ค)
pageLoadingInterval = setInterval(async () => {
if (!currentLoadingPdfPath) {
clearInterval(pageLoadingInterval);
$id('loadingPages').style.display = 'none';
return;
}
try {
const response = await fetch(`/api/cache-status?path=${encodeURIComponent(currentLoadingPdfPath)}`);
const status = await response.json();
// ์ƒํƒœ ์—…๋ฐ์ดํŠธ
if (status.status === "completed") {
clearInterval(pageLoadingInterval);
$id('loadingPages').style.display = 'none';
refreshFlipBook(); // ์™„๋ฃŒ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ๊ณ ์นจ
currentLoadingPdfPath = null;
} else if (status.status === "processing") {
$id('loadingPagesCount').textContent = `${status.progress}%`;
}
} catch (e) {
console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
}
}, 1000);
}
/* โ”€โ”€ ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ โ”€โ”€ */
async function refreshFlipBook() {
if (!currentLoadingPdfPath || !fb) return;
try {
const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(currentLoadingPdfPath)}`);
const cachedData = await response.json();
if(cachedData.status === "completed" && cachedData.pages) {
// ๊ธฐ์กด ํ”Œ๋ฆฝ๋ถ ์ •๋ฆฌ
fb.destroy();
viewer.innerHTML = '';
// ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ์žฌ์ƒ์„ฑ
createFlipBook(cachedData.pages);
currentLoadingPdfPath = null;
}
} catch (e) {
console.error("ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜:", e);
}
}
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';
// ์‚ฌ์šด๋“œ ์ดˆ๊ธฐํ™” ์—ฌ๋ถ€ ํ™•์ธ
if (!audioInitialized) {
initializeAudio()
.then(() => console.log('FlipBook ์ƒ์„ฑ ์ „ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์™„๋ฃŒ'))
.catch(e => console.warn('FlipBook ์ƒ์„ฑ ์‹œ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ:', e));
}
// ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ •์ œ (๋นˆ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ)
const validPages = pages.map(page => {
// src๊ฐ€ ์—†๋Š” ํŽ˜์ด์ง€๋Š” ๋กœ๋”ฉ ์ค‘ ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด
if (!page || !page.src) {
return {
src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+',
thumb: page && page.thumb ? page.thumb : ''
};
}
return page;
});
fb = new FlipBook(viewer, {
pages: validPages,
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: true, // ๊ณต์œ  ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
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, // ์ปจํŠธ๋กค ํ‘œ์‹œ ์‹œ๊ฐ„ ์—ฐ์žฅ
shareHandler: copyPdfShareUrl // ๊ณต์œ  ํ•ธ๋“ค๋Ÿฌ ์„ค์ •
}
});
// ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ 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์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
}
/* โ”€โ”€ ๋„ค๋น„๊ฒŒ์ด์…˜ โ”€โ”€ */
function toggle(showHome){
$id('home').style.display=showHome?'block':'none';
$id('viewerPage').style.display=showHome?'none':'block';
$id('homeButton').style.display=showHome?'none':'block';
$id('adminPage').style.display='none';
// AI ๋ฒ„ํŠผ ๊ด€๋ฆฌ
$id('aiButton').style.display = (!showHome && currentPdfId) ? 'block' : 'none';
// AI ์ฑ—๋ด‡์ด ์—ด๋ ค์žˆ์œผ๋ฉด ๋‹ซ๊ธฐ
if (isAiChatActive) {
toggleAiChat(false);
}
// ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
if(!showHome) {
document.body.classList.add('viewer-mode');
} else {
document.body.classList.remove('viewer-mode');
}
}
/* -- ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ -- */
function setupAdminFunctions() {
// ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ํด๋ฆญ - ๋ชจ๋‹ฌ ํ‘œ์‹œ
const adminButton = document.getElementById('adminButton');
const adminLoginModal = document.getElementById('adminLoginModal');
const adminLoginClose = document.getElementById('adminLoginClose');
const adminLoginButton = document.getElementById('adminLoginButton');
const adminPasswordInput = document.getElementById('adminPasswordInput');
const adminBackButton = document.getElementById('adminBackButton');
if (adminButton) {
adminButton.addEventListener('click', function() {
if (adminLoginModal) {
adminLoginModal.style.display = 'flex';
if (adminPasswordInput) {
adminPasswordInput.value = '';
adminPasswordInput.focus();
}
}
});
}
// ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ๋ฒ„ํŠผ
if (adminLoginClose) {
adminLoginClose.addEventListener('click', function() {
if (adminLoginModal) {
adminLoginModal.style.display = 'none';
}
});
}
// ์—”ํ„ฐ ํ‚ค๋กœ ๋กœ๊ทธ์ธ
if (adminPasswordInput) {
adminPasswordInput.addEventListener('keyup', function(e) {
if (e.key === 'Enter' && adminLoginButton) {
adminLoginButton.click();
}
});
}
// ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ
if (adminLoginButton) {
adminLoginButton.addEventListener('click', async function() {
if (!adminPasswordInput) return;
const password = adminPasswordInput.value;
try {
showLoading("๋กœ๊ทธ์ธ ์ค‘...");
const formData = new FormData();
formData.append('password', password);
const response = await fetch('/api/admin-login', {
method: 'POST',
body: formData
});
const data = await response.json();
hideLoading();
if (data.success) {
// ๋กœ๊ทธ์ธ ์„ฑ๊ณต - ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
if (adminLoginModal) {
adminLoginModal.style.display = 'none';
}
showAdminPage();
} else {
// ๋กœ๊ทธ์ธ ์‹คํŒจ
showError("๊ด€๋ฆฌ์ž ์ธ์ฆ ์‹คํŒจ: ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
}
} catch (error) {
console.error("๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:", error);
hideLoading();
showError("๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
});
}
// ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋’ค๋กœ๊ฐ€๊ธฐ
if (adminBackButton) {
adminBackButton.addEventListener('click', function() {
document.getElementById('adminPage').style.display = 'none';
document.getElementById('home').style.display = 'block';
});
}
}
// ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
async function showAdminPage() {
showLoading("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘...");
// ๋‹ค๋ฅธ ํŽ˜์ด์ง€ ์ˆจ๊ธฐ๊ธฐ
$id('home').style.display = 'none';
$id('viewerPage').style.display = 'none';
// ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์˜ PDF ๋ชฉ๋ก ๋กœ๋“œ
try {
const response = await fetch('/api/permanent-pdf-projects');
const data = await response.json();
const adminGrid = $id('adminGrid');
adminGrid.innerHTML = ''; // ๊ธฐ์กด ๋‚ด์šฉ ์ง€์šฐ๊ธฐ
if (data.length === 0) {
$id('noAdminProjects').style.display = 'block';
} else {
$id('noAdminProjects').style.display = 'none';
// ๊ฐ PDF ํŒŒ์ผ์— ๋Œ€ํ•œ ์นด๋“œ ์ƒ์„ฑ
const thumbnailPromises = data.map(async (pdf) => {
try {
// ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdf.path)}`);
const thumbData = await thumbResponse.json();
// ํ‘œ์‹œ ์—ฌ๋ถ€ ํ™•์ธ (๋ฉ”์ธ ํŽ˜์ด์ง€์— ํ‘œ์‹œ๋˜๋Š”์ง€)
const mainPdfPath = pdf.path.split('/').pop();
const isMainDisplayed = serverProjects.some(p => p.path.includes(mainPdfPath));
// ๊ด€๋ฆฌ์ž ์นด๋“œ ์ƒ์„ฑ
const card = document.createElement('div');
card.className = 'admin-card card fade-in';
// ๊ณ ์œ  URL ์ƒ์„ฑ
const viewUrl = `${window.location.origin}/view/${pdf.id}`;
// ์ธ๋„ค์ผ ๋ฐ ์ •๋ณด
card.innerHTML = `
<div class="card-inner">
${pdf.cached ? '<div class="cached-status">์บ์‹œ๋จ</div>' : ''}
<img src="${thumbData.thumbnail || ''}" alt="${pdf.name}" loading="lazy">
<p title="${pdf.name}">${pdf.name.length > 15 ? pdf.name.substring(0, 15) + '...' : pdf.name}</p>
<div style="position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%); z-index:10;">
<a href="${viewUrl}" target="_blank" style="color:#4a6ee0; font-size:12px;">๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ</a>
</div>
${isMainDisplayed ?
`<button class="unfeature-btn" data-path="${pdf.path}">๋ฉ”์ธ์—์„œ ์ œ๊ฑฐ</button>` :
`<button class="feature-btn" data-path="${pdf.path}">๋ฉ”์ธ์— ํ‘œ์‹œ</button>`}
<button class="delete-btn" data-path="${pdf.path}">์‚ญ์ œ</button>
</div>
`;
adminGrid.appendChild(card);
// ์‚ญ์ œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
const deleteBtn = card.querySelector('.delete-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', async function(e) {
e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
if (confirm(`์ •๋ง "${pdf.name}" PDF๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`)) {
try {
showLoading("PDF ์‚ญ์ œ ์ค‘...");
const response = await fetch(`/api/admin/delete-pdf?path=${encodeURIComponent(pdf.path)}`, {
method: 'DELETE'
});
const result = await response.json();
hideLoading();
if (result.success) {
card.remove();
showMessage("PDF๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
// ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
loadServerPDFs();
} else {
showError("์‚ญ์ œ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
}
} catch (error) {
console.error("PDF ์‚ญ์ œ ์˜ค๋ฅ˜:", error);
hideLoading();
showError("PDF ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
}
});
}
// ๋ฉ”์ธ์— ํ‘œ์‹œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
const featureBtn = card.querySelector('.feature-btn');
if (featureBtn) {
featureBtn.addEventListener('click', async function(e) {
e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
try {
showLoading("์ฒ˜๋ฆฌ ์ค‘...");
const response = await fetch(`/api/admin/feature-pdf?path=${encodeURIComponent(pdf.path)}`, {
method: 'POST'
});
const result = await response.json();
hideLoading();
if (result.success) {
showMessage("PDF๊ฐ€ ๋ฉ”์ธ ํŽ˜์ด์ง€์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.");
// ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ
showAdminPage();
// ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
loadServerPDFs();
} else {
showError("์ฒ˜๋ฆฌ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
}
} catch (error) {
console.error("PDF ํ‘œ์‹œ ์„ค์ • ์˜ค๋ฅ˜:", error);
hideLoading();
showError("์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
});
}
// ๋ฉ”์ธ์—์„œ ์ œ๊ฑฐ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
const unfeatureBtn = card.querySelector('.unfeature-btn');
if (unfeatureBtn) {
unfeatureBtn.addEventListener('click', async function(e) {
e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
try {
showLoading("์ฒ˜๋ฆฌ ์ค‘...");
const response = await fetch(`/api/admin/unfeature-pdf?path=${encodeURIComponent(pdf.path)}`, {
method: 'DELETE'
});
const result = await response.json();
hideLoading();
if (result.success) {
showMessage("PDF๊ฐ€ ๋ฉ”์ธ ํŽ˜์ด์ง€์—์„œ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
// ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ
showAdminPage();
// ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
loadServerPDFs();
} else {
showError("์ฒ˜๋ฆฌ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
}
} catch (error) {
console.error("PDF ํ‘œ์‹œ ํ•ด์ œ ์˜ค๋ฅ˜:", error);
hideLoading();
showError("์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
}
});
}
} catch (error) {
console.error(`PDF ${pdf.name} ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:`, error);
}
});
await Promise.all(thumbnailPromises);
}
// ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
hideLoading();
$id('adminPage').style.display = 'block';
} catch (error) {
console.error("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
hideLoading();
showError("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
$id('home').style.display = 'block'; // ์˜ค๋ฅ˜ ์‹œ ํ™ˆ์œผ๋กœ ๋ณต๊ท€
}
}
/* -- ๋กœ๋”ฉ ๋ฐ ์˜ค๋ฅ˜ ํ‘œ์‹œ -- */
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);
}
function showMessage(message) {
// ๊ธฐ์กด ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
const existingMessage = $id('messageContainer');
if (existingMessage) {
existingMessage.remove();
}
const messageContainer = document.createElement('div');
messageContainer.className = 'loading-container fade-in';
messageContainer.id = 'messageContainer';
messageContainer.innerHTML = `
<p class="loading-text" style="color: #2ecc71;">${message}</p>
<button id="messageCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">ํ™•์ธ</button>
`;
document.body.appendChild(messageContainer);
// ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
$id('messageCloseBtn').onclick = () => {
messageContainer.remove();
};
// 3์ดˆ ํ›„ ์ž๋™์œผ๋กœ ๋‹ซ๊ธฐ
setTimeout(() => {
if ($id('messageContainer')) {
$id('messageContainer').remove();
}
}, 3000);
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))