Files
Aislo/common_util/common_util_help_router.py
T
eomsangdonandClaude Opus 5.5 02f15c3d6a feat(가이드): 538-2 [?] = 넓은 모달 한 가이드 · 차례 묶음 → 페이지 → 기능 · 그림 · 주의/참고 · B01 [?]
- [?] = openModal wide · 왼쪽 차례(임도 설계 B → 안정성평가 E → 예정노선 D · 지금 페이지 펼침) ·
  오른쪽 = 모든 페이지 이어 그림 · 처음 = 보이는 data-help-topic 절 → 지금 페이지 절 → 맨 앞 · B05↔B06 짝 걷음
- 그리기: 덩어리마다 원문 줄(data-src) · `![설명](이름.webp)` → figure(loading=lazy · ?v=판) ·
  없는 그림 = 관리자 「그림 없음 — 더블클릭해 지정」 / 일반 「그림 준비 중」 · `> 주의:` · `> 참고:` 상자 ·
  `[글](B06/00_설명서)` → 모달 안 이동 · 페이지를 안 넘기면(B01 지식 채팅) 옛 출력 그대로
- GET /api/help 에 페이지마다 그림 판 · GET /api/help/{page}/img/{name}(이름 규칙 · no-cache) ·
  옛 GET /api/help/{page} 걷음(모달이 한 가이드로 바뀜)
- B01 대시보드 머리 역할 배지 왼쪽에 [?]
- 시험 test_538_help_guide.py(변환 글 · 그림 404 · admin 칸) + helper_538_help_render.cjs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:47:38 +09:00

108 lines
3.8 KiB
Python

"""[?] 사용자 가이드(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"})