"""[?] 사용자 가이드(54-2 · 538) — `A05_EduDetail/guide/<페이지>/*.md` 를 부를 때마다 읽어 줌. 사용자가 메모장으로 파일을 고치면 화면 고침 없이 다음에 열 때 바뀐 글이 보임. 파일 이름 순서 = 모달 왼쪽 차례 순서 · 제목 = 첫 `# ` 줄(없으면 파일 이름). `_` 로 시작하는 파일 · 그림은 항목 아님. 판(version) = 파일 sha256 앞 16자 · 페이지 제목 = `00_설명서` 의 제목. 그림 = 페이지 폴더의 `이름.webp|png|jpg` · 판 = 고친 시각 · 내려주기는 로그인만 · `no-cache`. """ from __future__ import annotations import hashlib import re from pathlib import Path from typing import Any from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse from common_util.common_util_auth import verify_session router = APIRouter(prefix="/api/help", tags=["help"]) HELP_ROOT = Path(__file__).resolve().parent.parent / "A05_EduDetail" / "guide" PAGE_RE = re.compile(r"^[BDE]\d{2}$") IMG_RE = re.compile(r"^[A-Za-z0-9_-]+\.(webp|png|jpg)$") def file_version(blob: bytes) -> str: return hashlib.sha256(blob).hexdigest()[:16] def image_version(path: Path) -> str: return format(path.stat().st_mtime_ns, "x") def page_folder(page: str, root: Path = HELP_ROOT) -> Path: if not PAGE_RE.match(page): raise HTTPException(status_code=404, detail="없는 페이지") return root / page def image_path(page: str, name: str, root: Path = HELP_ROOT) -> Path: if not IMG_RE.match(name): raise HTTPException(status_code=404, detail="없는 그림") return page_folder(page, root) / name def read_images(folder: Path) -> dict[str, str]: return {p.name: image_version(p) for p in sorted(folder.iterdir()) if IMG_RE.match(p.name)} def read_help(page: str, root: Path = HELP_ROOT) -> list[dict[str, str]]: folder = page_folder(page, root) items = [] for path in sorted(folder.glob("*.md")) if folder.is_dir() else []: if path.name.startswith("_"): continue blob = path.read_bytes() text = blob.decode("utf-8-sig") head = next((line[2:].strip() for line in text.splitlines() if line.startswith("# ")), "") items.append( { "key": path.stem, "title": head or path.stem, "text": text, "version": file_version(blob), } ) return items def read_guide(root: Path = HELP_ROOT) -> list[dict[str, Any]]: """모든 페이지 — 폴더 이름 순 · 글 없는 폴더는 뺌.""" pages = [] for folder in sorted(p for p in root.iterdir() if p.is_dir() and PAGE_RE.match(p.name)): items = read_help(folder.name, root) if not items: continue manual = next((i for i in items if i["key"] == "00_설명서"), None) pages.append( { "page": folder.name, "title": manual["title"] if manual else folder.name, "items": items, "images": read_images(folder), } ) return pages @router.get("") async def get_guide(session: dict[str, Any] = Depends(verify_session)) -> dict[str, Any]: return {"pages": read_guide(HELP_ROOT), "admin": session.get("role") == "SYSTEM_ADMIN"} MEDIA = {b"RIFF": "image/webp", b"\x89PNG": "image/png"} @router.get("/{page}/img/{name}") def get_image(page: str, name: str) -> FileResponse: path = image_path(page, name, HELP_ROOT) if not path.is_file(): raise HTTPException(status_code=404, detail="없는 그림") with path.open("rb") as f: # 올린 그림은 이름과 달리 WebP — 머리 글자로 가름 media = MEDIA.get(f.read(4), "image/jpeg") return FileResponse(path, media_type=media, headers={"Cache-Control": "no-cache"})