"""성장일지 — git 변경이력 자동 생성·조회.""" from __future__ import annotations import asyncio, logging, subprocess from datetime import datetime from fastapi import APIRouter, BackgroundTasks, Depends, Query from pydantic import BaseModel from sqlalchemy import select, desc from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user, require_admin_role from database import get_db from models import User, ChangelogEntry logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/changelog", tags=["성장일지-변경이력"]) GIT_ROOT = "/opt/guardia" def _run_git(cmd: list[str]) -> str: try: r = subprocess.run( ["git", "-C", GIT_ROOT] + cmd, capture_output=True, text=True, timeout=15 ) return r.stdout.strip() except Exception as e: return str(e) async def _collect_and_save(): """최신 커밋 50개에서 ChangelogEntry 자동 생성.""" log = _run_git([ "log", "--oneline", "--no-merges", "-50", "--format=%H|||%s|||%an|||%ad", "--date=short" ]) if not log: return 0 from database import AsyncSessionLocal async with AsyncSessionLocal() as db: count = 0 for line in log.splitlines(): parts = line.split("|||") if len(parts) < 4: continue commit_hash, subject, author, date = parts[0], parts[1], parts[2], parts[3] existing = await db.execute( select(ChangelogEntry).where(ChangelogEntry.commit_hash == commit_hash).limit(1) ) if existing.scalar_one_or_none(): continue cat = "feat" for prefix in ("fix", "docs", "refactor", "test", "chore", "style"): if subject.lower().startswith(prefix): cat = prefix break entry = ChangelogEntry( version="auto", category=cat, title=subject[:290], commit_hash=commit_hash, author=author, created_at=datetime.fromisoformat(date) if date else datetime.utcnow(), ) db.add(entry) count += 1 await db.commit() return count @router.post("/collect") async def collect_changelog( background_tasks: BackgroundTasks, user: User = Depends(require_admin_role), ): """git 커밋 이력에서 변경이력 자동 수집.""" background_tasks.add_task(_collect_and_save) return {"ok": True, "message": "변경이력 수집 시작됨 (백그라운드)"} @router.get("/list") async def list_changelog( limit: int = 50, category: str = Query(None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """변경이력 목록.""" q = select(ChangelogEntry).order_by(desc(ChangelogEntry.created_at)).limit(limit) if category: q = q.where(ChangelogEntry.category == category) rows = await db.execute(q) return [{ "id": e.id, "version": e.version, "category": e.category, "title": e.title, "author": e.author, "commit_hash": e.commit_hash, "created_at": e.created_at, } for e in rows.scalars().all()] @router.get("/summary") async def changelog_summary(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """카테고리별 변경 수 요약.""" from sqlalchemy import func as sqlfunc rows = await db.execute( select(ChangelogEntry.category, sqlfunc.count().label("cnt")) .group_by(ChangelogEntry.category) .order_by(desc("cnt")) ) return {"summary": [{"category": r.category, "count": r.cnt} for r in rows.all()]} class ManualEntry(BaseModel): version: str category: str = "feat" title: str description: str = "" @router.post("/entry") async def add_entry( entry: ManualEntry, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role), ): """수동 변경이력 추가.""" row = ChangelogEntry( version=entry.version, category=entry.category, title=entry.title, description=entry.description, author=user.username, created_at=datetime.utcnow(), ) db.add(row) await db.commit() await db.refresh(row) return {"id": row.id, "ok": True}