"""성장일지 — 기능 성장 대시보드 (라우터 수·SR 처리·자립도 추이).""" from __future__ import annotations import logging from datetime import datetime, timedelta from pathlib import Path from fastapi import APIRouter, Depends from sqlalchemy import select, func as sqlfunc, desc from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import User, ChangelogEntry, HealthCheckResult, SelfReport, IndependenceScore logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/growth", tags=["성장일지-대시보드"]) ITSM_ROUTER_DIR = Path("/opt/guardia/workspace/guardia-itsm/routers") def _count_routers() -> int: if not ITSM_ROUTER_DIR.exists(): return 0 return len([f for f in ITSM_ROUTER_DIR.glob("*.py") if not f.name.startswith("_")]) @router.get("/dashboard") async def growth_dashboard( db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """GUARDiA 성장 현황 종합 대시보드.""" router_count = _count_routers() # 최근 건강검진 결과 hc_row = await db.execute( select(HealthCheckResult).order_by(desc(HealthCheckResult.created_at)).limit(1) ) last_hc = hc_row.scalar_one_or_none() # 최근 자립도 점수 score_row = await db.execute( select(IndependenceScore) .where(IndependenceScore.dimension == "overall") .order_by(desc(IndependenceScore.measured_at)) .limit(1) ) last_score = score_row.scalar_one_or_none() # 변경이력 30일 집계 thirty_days_ago = datetime.utcnow() - timedelta(days=30) cl_count = await db.execute( select(sqlfunc.count()).select_from(ChangelogEntry) .where(ChangelogEntry.created_at >= thirty_days_ago) ) # 주간 보고서 수 report_count = await db.execute( select(sqlfunc.count()).select_from(SelfReport) ) return { "router_count": router_count, "estimated_endpoints": router_count * 7, "health": { "last_check": last_hc.created_at if last_hc else None, "status": "HEALTHY" if (last_hc and last_hc.success) else "UNKNOWN", "passed": last_hc.passed if last_hc else 0, "total": last_hc.total if last_hc else 69, }, "independence": { "score": last_score.score if last_score else 30.0, "target": last_score.target_score if last_score else 85.0, "dimension": last_score.dimension if last_score else "overall", }, "changelog_30d": cl_count.scalar() or 0, "weekly_reports": report_count.scalar() or 0, "milestones": [ {"label": "라우터 100개", "achieved": router_count >= 100}, {"label": "자립도 50%", "achieved": (last_score.score if last_score else 0) >= 50}, {"label": "자립도 70%", "achieved": (last_score.score if last_score else 0) >= 70}, {"label": "자립도 85%", "achieved": (last_score.score if last_score else 0) >= 85}, ], "generated_at": datetime.utcnow().isoformat(), } @router.get("/timeline") async def growth_timeline( days: int = 90, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """날짜별 변경이력 타임라인.""" since = datetime.utcnow() - timedelta(days=days) rows = await db.execute( select(ChangelogEntry).where(ChangelogEntry.created_at >= since) .order_by(ChangelogEntry.created_at) .limit(200) ) return [{ "date": e.created_at.strftime("%Y-%m-%d"), "category": e.category, "title": e.title, "author": e.author, } for e in rows.scalars().all()]