210 lines
9.9 KiB
Python
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": "특이 사항 없음",
|
|
}
|