guardia-itsm/routers/mobile2_ext.py
2026-06-07 00:13:38 +09:00

210 lines
9.9 KiB
Python

"""
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": "특이 사항 없음",
}