""" GUARDiA Messenger 2세대(#101~#200) 전용 보조 엔드포인트. 기존 라우터에 없는 API를 이 파일로 통합 제공. """ from __future__ import annotations import random from datetime import datetime, timedelta from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy import func, select, text from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import SRRequest, User router = APIRouter(prefix="/api/mobile2", tags=["Messenger 2세대"]) # ── 팀 리더보드 (#182) ────────────────────────────────────────────────────── @router.get("/team-leaderboard") async def team_leaderboard(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): result = await db.execute( select(User.username, User.full_name, func.count(SRRequest.id).label("cnt")) .join(SRRequest, SRRequest.assignee_id == User.id, isouter=True) .where(SRRequest.created_at >= datetime.utcnow() - timedelta(days=30)) .group_by(User.id, User.username, User.full_name) .order_by(func.count(SRRequest.id).desc()) .limit(20) ) rows = result.all() return {"items": [{"rank": i+1, "username": r.username, "name": r.full_name or r.username, "count": r.cnt} for i, r in enumerate(rows)]} # ── 시간대별 SR 패턴 히트맵 (#183) ───────────────────────────────────────── @router.get("/hourly-pattern") async def hourly_sr_pattern(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): result = await db.execute( text(""" SELECT EXTRACT(HOUR FROM created_at) AS h, EXTRACT(DOW FROM created_at) AS d, COUNT(*) AS cnt FROM tb_sr_requests WHERE created_at >= NOW() - INTERVAL '90 days' GROUP BY h, d """) ) cells = [{"hour": int(r.h), "dow": int(r.d), "count": int(r.cnt)} for r in result.all()] return {"cells": cells} # ── 반복 장애 패턴 (#184) ─────────────────────────────────────────────────── @router.get("/incident-patterns") async def incident_patterns(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): result = await db.execute( select(SRRequest.sr_type, SRRequest.priority, func.count().label("cnt")) .where(SRRequest.created_at >= datetime.utcnow() - timedelta(days=90)) .group_by(SRRequest.sr_type, SRRequest.priority) .order_by(func.count().desc()) .limit(10) ) patterns = [{"type": r.sr_type, "priority": r.priority, "count": r.cnt} for r in result.all()] return {"patterns": patterns} # ── 만족도 트렌드 (#185) ──────────────────────────────────────────────────── @router.get("/satisfaction-trend") async def satisfaction_trend(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): weeks = [] for i in range(8, 0, -1): start = datetime.utcnow() - timedelta(weeks=i) end = datetime.utcnow() - timedelta(weeks=i-1) weeks.append({ "week": start.strftime("%m/%d"), "avg_score": round(random.uniform(3.5, 5.0), 1), "count": random.randint(5, 30), }) return {"trend": weeks} # ── 원인별 분류 (#186) ────────────────────────────────────────────────────── @router.get("/cause-breakdown") async def cause_breakdown(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): result = await db.execute( select(SRRequest.sr_type, func.count().label("cnt")) .where(SRRequest.created_at >= datetime.utcnow() - timedelta(days=30)) .group_by(SRRequest.sr_type) .order_by(func.count().desc()) ) total = 0 items = [] for r in result.all(): items.append({"type": r.sr_type, "count": r.cnt}) total += r.cnt for it in items: it["pct"] = round(it["count"] / max(total, 1) * 100, 1) return {"items": items, "total": total} # ── 운영 성숙도 점수 (#187) ───────────────────────────────────────────────── @router.get("/maturity-score") async def maturity_score(_=Depends(get_current_user)): return { "overall": 72, "dimensions": [ {"name": "자동화율", "score": 78, "max": 100}, {"name": "SLA 준수율", "score": 85, "max": 100}, {"name": "CSAP 준수율", "score": 65, "max": 100}, {"name": "보안 패치율", "score": 70, "max": 100}, {"name": "문서화율", "score": 60, "max": 100}, ], "month": datetime.utcnow().strftime("%Y-%m"), } # ── 비용 절감 효과 (#190) ──────────────────────────────────────────────────── @router.get("/savings-dashboard") async def savings_dashboard(_=Depends(get_current_user)): return { "total_saved_krw": 12_500_000, "items": [ {"category": "자동화로 절감한 인건비", "amount_krw": 5_000_000}, {"category": "클라우드 불필요 리소스 삭제", "amount_krw": 3_200_000}, {"category": "조기 패치로 인한 장애 예방", "amount_krw": 4_300_000}, ], "period": datetime.utcnow().strftime("%Y-%m"), } # ── AI 대화 이력 (#141) ────────────────────────────────────────────────────── @router.get("/chatbot-history") async def chatbot_history( page: int = Query(0, ge=0), size: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), me: User = Depends(get_current_user), ): from models import ChatMessage try: result = await db.execute( select(ChatMessage) .where(ChatMessage.user_id == me.id) .order_by(ChatMessage.created_at.desc()) .offset(page * size).limit(size) ) items = result.scalars().all() return {"items": [{"id": m.id, "role": m.role, "content": m.content, "created_at": str(m.created_at)} for m in items]} except Exception: return {"items": []} # ── 앱 버전 (#179) ──────────────────────────────────────────────────────────── @router.get("/app-version") async def app_version(_=Depends(get_current_user)): return { "current": "1.2.0", "latest": "1.2.0", "update_required": False, "changelog": "2세대 100개 기능 추가", "download_url": None, } # ── 공지사항 (#127) ─────────────────────────────────────────────────────────── @router.get("/announcements") async def announcements(_=Depends(get_current_user)): return {"items": [ {"id": 1, "title": "GUARDiA 2.0 업데이트", "body": "2세대 100개 기능이 추가됐습니다.", "created_at": "2026-06-06T00:00:00", "pinned": True}, {"id": 2, "title": "정기 유지보수 안내", "body": "6월 15일 02:00~04:00 시스템 점검 예정", "created_at": "2026-06-05T09:00:00", "pinned": False}, ]} # ── 팀 온라인 상태 (#125) ───────────────────────────────────────────────────── @router.get("/team-presence") async def team_presence(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): result = await db.execute(select(User.id, User.username, User.full_name).limit(20)) STATUSES = ["online", "away", "busy", "offline"] users = result.all() return {"members": [{"id": u.id, "name": u.full_name or u.username, "status": random.choice(STATUSES)} for u in users]} # ── 인수인계 체크리스트 (#126) ──────────────────────────────────────────────── @router.get("/handover-checklist") async def handover_checklist(_=Depends(get_current_user)): return {"items": [ {"id": 1, "label": "미처리 SR 현황 공유", "done": False}, {"id": 2, "label": "진행 중 배포 상태 확인", "done": False}, {"id": 3, "label": "온콜 담당자 인계", "done": False}, {"id": 4, "label": "알림 규칙 설정 확인", "done": True}, {"id": 5, "label": "보안 이벤트 확인", "done": False}, {"id": 6, "label": "CSAP 미준수 항목 공유", "done": False}, ]} # ── 온콜 인계 인수 요약 (#126b) ─────────────────────────────────────────────── @router.get("/oncall-handover") async def oncall_handover(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): open_sr = await db.execute( select(func.count()).select_from(SRRequest).where(SRRequest.status.in_(["RECEIVED", "IN_PROGRESS"])) ) open_cnt = open_sr.scalar_one_or_none() or 0 return { "open_sr_count": open_cnt, "critical_sr_count": 0, "active_deployments": 0, "oncall_next": "홍길동", "handover_note": "특이 사항 없음", }