feat(가이드): 538-2 [?] = 넓은 모달 한 가이드 · 차례 묶음 → 페이지 → 기능 · 그림 · 주의/참고 · B01 [?]
- [?] = openModal wide · 왼쪽 차례(임도 설계 B → 안정성평가 E → 예정노선 D · 지금 페이지 펼침) ·
오른쪽 = 모든 페이지 이어 그림 · 처음 = 보이는 data-help-topic 절 → 지금 페이지 절 → 맨 앞 · B05↔B06 짝 걷음
- 그리기: 덩어리마다 원문 줄(data-src) · `` → 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>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
파일 이름 순서 = 모달 왼쪽 차례 순서 · 제목 = 첫 `# ` 줄(없으면 파일 이름).
|
||||
`_` 로 시작하는 파일 · 그림은 항목 아님.
|
||||
판(version) = 파일 sha256 앞 16자 · 페이지 제목 = `00_설명서` 의 제목.
|
||||
그림 = 페이지 폴더의 `이름.webp|png|jpg` · 판 = 고친 시각 · 내려주기는 로그인만 · `no-cache`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -14,6 +15,7 @@ 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
|
||||
|
||||
@@ -21,16 +23,35 @@ 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 read_help(page: str, root: Path = HELP_ROOT) -> list[dict[str, str]]:
|
||||
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="없는 페이지")
|
||||
folder = root / page
|
||||
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("_"):
|
||||
@@ -62,6 +83,7 @@ def read_guide(root: Path = HELP_ROOT) -> list[dict[str, Any]]:
|
||||
"page": folder.name,
|
||||
"title": manual["title"] if manual else folder.name,
|
||||
"items": items,
|
||||
"images": read_images(folder),
|
||||
}
|
||||
)
|
||||
return pages
|
||||
@@ -69,9 +91,17 @@ def read_guide(root: Path = HELP_ROOT) -> list[dict[str, Any]]:
|
||||
|
||||
@router.get("")
|
||||
async def get_guide(session: dict[str, Any] = Depends(verify_session)) -> dict[str, Any]:
|
||||
return {"pages": read_guide(), "admin": session.get("role") == "SYSTEM_ADMIN"}
|
||||
return {"pages": read_guide(HELP_ROOT), "admin": session.get("role") == "SYSTEM_ADMIN"}
|
||||
|
||||
|
||||
@router.get("/{page}")
|
||||
async def get_help(page: str) -> dict[str, list[dict[str, str]]]:
|
||||
return {"items": read_help(page)}
|
||||
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"})
|
||||
|
||||
Reference in New Issue
Block a user