"""독립 지원 — 자립도 점수 측정·추적·로드맵.""" from __future__ import annotations import logging from datetime import datetime, timedelta from fastapi import APIRouter, BackgroundTasks, Depends from sqlalchemy import select, desc, func as sqlfunc 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, IndependenceScore, HealthCheckResult, AutonomousAction, SelfReport logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/independence", tags=["독립지원-자립도"]) # 자립도 차원별 가중치 (합산 100%) DIMENSIONS = { "health": 0.25, # 건강검진 자동화 "sr_auto": 0.30, # SR AI 자동처리율 "self_heal": 0.20, # 자가수복 성공률 "report": 0.10, # 자율 보고서 발송 "finetune": 0.15, # AI 파인튜닝 자동화 } ROADMAP = [ {"milestone": "현재", "target": 30, "description": "대부분 수동 개입 필요"}, {"milestone": "3개월", "target": 50, "description": "SR의 절반 AI 자동처리"}, {"milestone": "6개월", "target": 70, "description": "장애 자동수복 + 자동 파인튜닝"}, {"milestone": "1년", "target": 85, "description": "GUARDiA가 알아서 다 했습니다"}, ] async def _measure_independence() -> dict: """각 차원별 점수 계산 → 자립도 점수 산출.""" from database import AsyncSessionLocal async with AsyncSessionLocal() as db: since = datetime.utcnow() - timedelta(days=30) # 1. 건강검진 자동화 (최근 30일 실행 횟수 기준, 30회 이상 = 100점) hc_count = (await db.execute( select(sqlfunc.count()).select_from(HealthCheckResult) .where(HealthCheckResult.created_at >= since, HealthCheckResult.triggered_by.like("schedule%")) )).scalar() or 0 health_score = min(hc_count / 30 * 100, 100) # 2. SR 자동처리율 try: from models import ServiceRequest sr_total = (await db.execute( select(sqlfunc.count()).select_from(ServiceRequest) .where(ServiceRequest.created_at >= since) )).scalar() or 0 sr_auto = (await db.execute( select(sqlfunc.count()).select_from(ServiceRequest) .where(ServiceRequest.created_at >= since, ServiceRequest.resolved_by_ai == True) )).scalar() or 0 sr_score = (sr_auto / sr_total * 100) if sr_total else 0 except Exception: sr_score = 0 # 3. 자가수복 성공률 heal_total = (await db.execute( select(sqlfunc.count()).select_from(AutonomousAction) .where(AutonomousAction.executed_at >= since, AutonomousAction.action_type == "restart") )).scalar() or 0 heal_ok = (await db.execute( select(sqlfunc.count()).select_from(AutonomousAction) .where(AutonomousAction.executed_at >= since, AutonomousAction.action_type == "restart", AutonomousAction.success == True) )).scalar() or 0 heal_score = (heal_ok / heal_total * 100) if heal_total else 0 # 4. 자율 보고서 발송 (월 4회 이상 = 100점) report_count = (await db.execute( select(sqlfunc.count()).select_from(SelfReport) .where(SelfReport.created_at >= since) )).scalar() or 0 report_score = min(report_count / 4 * 100, 100) # 5. 파인튜닝 자동화 (최근 완료 작업 수) from models import AutoFinetuneJob ft_count = (await db.execute( select(sqlfunc.count()).select_from(AutoFinetuneJob) .where(AutoFinetuneJob.created_at >= since, AutoFinetuneJob.status == "success") )).scalar() or 0 ft_score = min(ft_count / 1 * 100, 100) dim_scores = { "health": health_score, "sr_auto": sr_score, "self_heal": heal_score, "report": report_score, "finetune": ft_score, } overall = sum(dim_scores[d] * DIMENSIONS[d] for d in DIMENSIONS) # 저장 entry = IndependenceScore( score=round(overall, 2), dimension="overall", details=str(dim_scores), target_score=85.0, measured_at=datetime.utcnow(), ) db.add(entry) for dim, score in dim_scores.items(): db.add(IndependenceScore( score=round(score, 2), dimension=dim, target_score=100.0, measured_at=datetime.utcnow(), )) await db.commit() return {"overall": round(overall, 2), "dimensions": dim_scores} @router.post("/measure") async def measure( background_tasks: BackgroundTasks, user: User = Depends(require_admin_role), ): """자립도 즉시 측정.""" background_tasks.add_task(_measure_independence) return {"ok": True, "message": "자립도 측정 시작됨 (백그라운드)"} @router.get("/score") async def get_score( db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """현재 자립도 점수.""" row = await db.execute( select(IndependenceScore) .where(IndependenceScore.dimension == "overall") .order_by(desc(IndependenceScore.measured_at)).limit(1) ) latest = row.scalar_one_or_none() return { "score": latest.score if latest else 30.0, "target": latest.target_score if latest else 85.0, "measured_at": latest.measured_at if latest else None, "roadmap": ROADMAP, } @router.get("/history") async def score_history( limit: int = 30, dimension: str = "overall", db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """자립도 추이 (차트용).""" rows = await db.execute( select(IndependenceScore) .where(IndependenceScore.dimension == dimension) .order_by(desc(IndependenceScore.measured_at)).limit(limit) ) return [{ "score": s.score, "dimension": s.dimension, "measured_at": s.measured_at, } for s in reversed(rows.scalars().all())] @router.get("/roadmap") async def roadmap(user: User = Depends(get_current_user)): """자립도 로드맵.""" return {"roadmap": ROADMAP, "dimensions": DIMENSIONS}