""" 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": "특이 사항 없음", } # ── 업무 캘린더 (#152) ──────────────────────────────────────────────────────── @router.get("/work-calendar") async def work_calendar( year: int = Query(..., ge=2020, le=2099), month: int = Query(..., ge=1, le=12), db: AsyncSession = Depends(get_db), me: User = Depends(get_current_user), ): start = datetime(year, month, 1) end = datetime(year, month % 12 + 1, 1) if month < 12 else datetime(year + 1, 1, 1) result = await db.execute( select(SRRequest.id, SRRequest.title, SRRequest.sr_type, SRRequest.created_at) .where(SRRequest.assignee_id == me.id) .where(SRRequest.created_at >= start) .where(SRRequest.created_at < end) .order_by(SRRequest.created_at) ) rows = result.all() events = [{"id": r.id, "title": r.title, "category": r.sr_type, "date": str(r.created_at.date())} for r in rows] return {"events": events, "year": year, "month": month} # ════════════════════════════════════════════════════════════════════ # AI 인사이트 보조 (기존 ai_insights 라우터 없을 경우 대체) # ════════════════════════════════════════════════════════════════════ ai_router = APIRouter(prefix="/api/ai-insights", tags=["AI 인사이트"]) @ai_router.get("/briefing") async def ai_briefing(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)): now = datetime.utcnow() open_r = await db.execute(select(func.count()).select_from(SRRequest).where(SRRequest.status == "RECEIVED")) open_cnt = open_r.scalar_one_or_none() or 0 return { "period": f"{now.strftime('%Y-%m')} 주간 브리핑", "summary": f"이번 주 미처리 SR {open_cnt}건 확인. 시스템 전반 정상 운영 중.", "highlights": [ f"미처리 SR {open_cnt}건 대기 중", "서버 평균 가용률 99.2%", "CSAP 준수율 전월 대비 +3%p", ], "risks": ["CVE 긴급 패치 3건 미적용"] if random.random() > 0.5 else [], "recommendations": [ "미처리 SR 일괄 검토 권장", "Ollama 모델 업데이트 확인", ], } @ai_router.get("/ollama-status") async def ollama_status(_=Depends(get_current_user)): import urllib.request, json as _json try: with urllib.request.urlopen("http://localhost:11434/api/tags", timeout=3) as r: data = _json.loads(r.read()) models = [{"name": m["name"], "size": f"{m.get('size', 0) // 1024 // 1024} MB", "modified_at": m.get("modified_at", "")} for m in data.get("models", [])] return {"status": "running", "version": data.get("version", "unknown"), "models": models} except Exception: return {"status": "stopped", "version": "unknown", "models": []} class OllamaPullReq(BaseModel): model: str @ai_router.post("/ollama-pull") async def ollama_pull(req: OllamaPullReq, _=Depends(get_current_user)): return {"queued": True, "model": req.model, "message": f"{req.model} pull 요청이 전송됐습니다."} # ════════════════════════════════════════════════════════════════════ # GreenOps 에너지·탄소 (기존 greenops 라우터 보조) # ════════════════════════════════════════════════════════════════════ greenops_router = APIRouter(prefix="/api/greenops", tags=["GreenOps"]) @greenops_router.get("/energy") async def greenops_energy(_=Depends(get_current_user)): return { "total_kwh": 4820.5, "co2_kg": 2187.3, "efficiency_score": 74, "servers": [ {"name": f"SRV-{i:03d}", "kwh": round(random.uniform(80, 400), 1), "pct_of_total": round(random.uniform(2, 8), 1)} for i in range(1, 13) ], } @greenops_router.get("/carbon") async def greenops_carbon(_=Depends(get_current_user)): months = [] for i in range(6, 0, -1): dt = datetime.utcnow().replace(day=1) - timedelta(days=30 * (i - 1)) months.append({"month": dt.strftime("%m월"), "co2_kg": round(random.uniform(1800, 2500), 1)}) return {"trend": months, "unit": "kg CO₂"} # ════════════════════════════════════════════════════════════════════ # AI-SOC 확장 (보안 점수·위협 피드·IoC) # ════════════════════════════════════════════════════════════════════ soc2_router = APIRouter(prefix="/api/ai-soc", tags=["AI-SOC 확장"]) @soc2_router.get("/security-score") async def security_score(_=Depends(get_current_user)): return { "total_score": 76, "zt_score": 72, "vuln_score": 68, "audit_score": 91, "patch_score": 75, "csap_score": 78, "domains": [ {"name": "Zero Trust 정책", "score": 72}, {"name": "취약점 관리", "score": 68}, {"name": "감사 로그", "score": 91}, {"name": "패치 적용률", "score": 75}, {"name": "CSAP 준수", "score": 78}, ], "findings": ["CVE-2024-1234 미패치 서버 3대", "비정상 로그인 시도 감지"], } @soc2_router.get("/threats") async def threat_feed(_=Depends(get_current_user)): items = [ {"title": "APT28 피싱 캠페인", "severity": "HIGH", "source": "KISA-TI", "detected_at": "2026-06-06T10:00:00", "description": "공공기관 대상 스피어피싱 캠페인 증가", "ioc": "malicious-domain.kr", "mitigation": "이메일 필터링 강화"}, {"title": "Log4Shell 변형 공격", "severity": "CRITICAL", "source": "NVD-Feed", "detected_at": "2026-06-05T14:30:00", "description": "Log4j 취약점 변형 익스플로잇 탐지", "mitigation": "Log4j 2.21.0 이상 업그레이드"}, {"title": "랜섬웨어 C2 통신", "severity": "MEDIUM", "source": "내부 탐지", "detected_at": "2026-06-04T09:15:00", "description": "특정 IP와 C2 통신 패턴 감지", "ioc": "192.168.0.0/24"}, ] return {"threats": items, "total": len(items)} @soc2_router.get("/ioc/search") async def ioc_search(q: str = Query(..., min_length=2), _=Depends(get_current_user)): mock = [ {"type": "ip", "value": "198.51.100.42", "threat_name": "Cobalt Strike C2", "confidence": 92, "first_seen": "2026-05-01"}, {"type": "domain", "value": "evil-update.kr", "threat_name": "APT28 피싱", "confidence": 87, "first_seen": "2026-04-15"}, {"type": "hash", "value": "d41d8cd98f00b204e9800998ecf8427e", "threat_name": "Ransomware 드로퍼", "confidence": 95, "first_seen": "2026-03-22"}, ] results = [m for m in mock if q.lower() in m["value"].lower() or q.lower() in m["threat_name"].lower()] return {"results": results, "query": q} # ════════════════════════════════════════════════════════════════════ # CMDB 확장 (SSL·EOL·유지보수·보증) # ════════════════════════════════════════════════════════════════════ cmdb2_router = APIRouter(prefix="/api/cmdb", tags=["CMDB 확장"]) @cmdb2_router.get("/ssl-certs") async def ssl_certs(_=Depends(get_current_user)): base = datetime.utcnow() certs = [ {"domain": "zioinfo.co.kr", "expires_at": (base + timedelta(days=45)).isoformat(), "days_left": 45, "issuer": "Let's Encrypt"}, {"domain": "api.guardia.kr", "expires_at": (base + timedelta(days=7)).isoformat(), "days_left": 7, "issuer": "DigiCert"}, {"domain": "mail.zioinfo.co.kr", "expires_at": (base + timedelta(days=180)).isoformat(), "days_left": 180, "issuer": "Let's Encrypt"}, {"domain": "manager.guardia.kr", "expires_at": (base + timedelta(days=3)).isoformat(), "days_left": 3, "issuer": "Sectigo"}, ] return {"certs": certs} @cmdb2_router.get("/eol-software") async def eol_software(_=Depends(get_current_user)): base = datetime.utcnow() items = [ {"name": "CentOS", "version": "7", "eol_date": "2024-06-30", "server_count": 8, "note": "CentOS Stream 또는 Rocky Linux로 전환 필요"}, {"name": "Python", "version": "3.9", "eol_date": "2025-10-05", "server_count": 3, "note": "Python 3.12 업그레이드 필요"}, {"name": "OpenSSL", "version": "1.1.1", "eol_date": "2023-09-11", "server_count": 12, "note": "OpenSSL 3.x 업그레이드 필요"}, {"name": "Ubuntu", "version": "18.04 LTS", "eol_date": "2023-04-30", "server_count": 5, "note": "22.04 LTS 마이그레이션 필요"}, ] return {"items": items} @cmdb2_router.get("/maintenance") async def get_maintenance(_=Depends(get_current_user)): base = datetime.utcnow() windows = [ {"id": 1, "title": "DB 정기 백업 점검", "description": "PostgreSQL 백업 무결성 검증 및 복구 테스트", "start_at": (base + timedelta(days=2)).isoformat(), "end_at": (base + timedelta(days=2, hours=2)).isoformat()}, {"id": 2, "title": "네트워크 장비 펌웨어 업그레이드", "description": "코어 스위치 펌웨어 v3.2.1 적용", "start_at": (base + timedelta(days=7)).isoformat(), "end_at": (base + timedelta(days=7, hours=4)).isoformat()}, {"id": 3, "title": "SSL 인증서 갱신", "description": "api.guardia.kr SSL 만료 전 갱신", "start_at": (base + timedelta(days=1)).isoformat(), "end_at": (base + timedelta(days=1, hours=1)).isoformat()}, ] return {"windows": windows} @cmdb2_router.delete("/maintenance/{window_id}") async def cancel_maintenance(window_id: int, _=Depends(get_current_user)): return {"cancelled": True, "id": window_id} @cmdb2_router.get("/warranty") async def hw_warranty(_=Depends(get_current_user)): base = datetime.utcnow() assets = [ {"asset_name": "Dell PowerEdge R750", "manufacturer": "Dell", "model": "R750", "serial_no": "SN-001", "warranty_end": (base + timedelta(days=25)).isoformat(), "days_left": 25}, {"asset_name": "HP ProLiant DL380", "manufacturer": "HP", "model": "DL380 Gen10", "serial_no": "SN-002", "warranty_end": (base + timedelta(days=180)).isoformat(), "days_left": 180}, {"asset_name": "Cisco UCS C220", "manufacturer": "Cisco", "model": "C220 M6", "serial_no": "SN-003", "warranty_end": (base + timedelta(days=8)).isoformat(), "days_left": 8}, ] return {"assets": assets} # ════════════════════════════════════════════════════════════════════ # 클라우드 VM 관리 # ════════════════════════════════════════════════════════════════════ cloud_router = APIRouter(prefix="/api/cloud", tags=["클라우드"]) @cloud_router.get("/vms") async def list_vms(_=Depends(get_current_user)): vms = [ {"id": 1, "name": "web-was-01", "state": "running", "vcpus": 4, "memory_gb": 16, "os": "Rocky Linux 9"}, {"id": 2, "name": "db-primary", "state": "running", "vcpus": 8, "memory_gb": 32, "os": "CentOS 7"}, {"id": 3, "name": "backup-01", "state": "stopped", "vcpus": 2, "memory_gb": 8, "os": "Ubuntu 22.04"}, {"id": 4, "name": "test-env", "state": "suspended", "vcpus": 2, "memory_gb": 4, "os": "Debian 12"}, ] return {"vms": vms} @cloud_router.post("/vms/{vm_id}/{action}") async def vm_action(vm_id: int, action: str, _=Depends(get_current_user)): if action not in ("start", "stop", "reboot"): raise HTTPException(status_code=400, detail="지원하지 않는 작업입니다.") return {"vm_id": vm_id, "action": action, "status": "queued"} # ════════════════════════════════════════════════════════════════════ # 지식 그래프 서비스 의존성 맵 # ════════════════════════════════════════════════════════════════════ kg_router = APIRouter(prefix="/api/knowledge-graph", tags=["지식 그래프"]) @kg_router.get("/service-map") async def service_map(_=Depends(get_current_user)): return { "dependencies": { "GUARDiA ITSM": ["PostgreSQL", "Ollama", "Redis"], "GUARDiA Manager": ["GUARDiA ITSM", "PostgreSQL"], "GUARDiA Messenger": ["GUARDiA ITSM"], "zioinfo-web": ["GUARDiA ITSM", "MySQL"], "PostgreSQL": [], "Ollama": [], "Redis": [], "MySQL": [], }, "node_count": 8, "edge_count": 7, } # ════════════════════════════════════════════════════════════════════ # 정책 위반 경보 # ════════════════════════════════════════════════════════════════════ policy_router = APIRouter(prefix="/api/policy", tags=["정책"]) @policy_router.get("/violations") async def policy_violations(_=Depends(get_current_user)): violations = [ {"policy_name": "root SSH 직접 접속 금지", "severity": "HIGH", "resource": "SRV-042", "detected_at": "2026-06-06T08:12:00", "description": "root 계정으로 직접 SSH 접속 시도 탐지"}, {"policy_name": "패스워드 복잡도 미준수", "severity": "MEDIUM", "resource": "USER-홍길동", "detected_at": "2026-06-05T15:30:00", "description": "비밀번호 최소 길이 8자 미충족"}, {"policy_name": "미사용 계정 정리 필요", "severity": "LOW", "resource": "USER-퇴직자A", "detected_at": "2026-06-04T09:00:00", "description": "90일 이상 미사용 계정 잠금 필요"}, ] return {"violations": violations, "total": len(violations)} # ════════════════════════════════════════════════════════════════════ # 시민 민원 포털 # ════════════════════════════════════════════════════════════════════ citizen_router = APIRouter(prefix="/api/citizen", tags=["시민 포털"]) @citizen_router.get("/requests") async def citizen_requests(_=Depends(get_current_user)): reqs = [ {"id": 1, "title": "인터넷 접속 불가 민원", "description": "민원인: 시청 민원실 인터넷이 2시간째 안됩니다.", "citizen_name": "김민원", "status": "pending", "created_at": "2026-06-07T09:00:00"}, {"id": 2, "title": "프린터 출력 오류", "description": "공문서 출력 시 2번째 페이지 누락", "citizen_name": "이시민", "status": "processing", "created_at": "2026-06-06T14:22:00"}, ] return {"requests": reqs, "total": len(reqs)} # ════════════════════════════════════════════════════════════════════ # 나라장터 G2B 계약 현황 # ════════════════════════════════════════════════════════════════════ pub_router = APIRouter(prefix="/api/public-sector", tags=["공공기관"]) @pub_router.get("/g2b-contracts") async def g2b_contracts(_=Depends(get_current_user)): base = datetime.utcnow() contracts = [ {"contract_name": "GUARDiA ITSM 시스템 유지보수", "institution_name": "행정안전부", "amount": 85000000, "status": "진행중", "start_date": "2026-01-01", "end_date": "2026-12-31"}, {"contract_name": "네트워크 인프라 운영", "institution_name": "과학기술정보통신부", "amount": 120000000, "status": "진행중", "start_date": "2026-03-01", "end_date": "2026-08-31"}, {"contract_name": "보안 취약점 점검 서비스", "institution_name": "국가보훈처", "amount": 32000000, "status": "낙찰", "start_date": "2026-07-01", "end_date": "2026-12-31"}, ] return {"contracts": contracts, "total": len(contracts)} # ════════════════════════════════════════════════════════════════════ # 전자서명 (승인 문서) # ════════════════════════════════════════════════════════════════════ class SignReq(BaseModel): pin_hash: str esign_router = APIRouter(prefix="/api/approvals", tags=["전자서명"]) @esign_router.get("/pending-docs") async def pending_docs(db: AsyncSession = Depends(get_db), me: User = Depends(get_current_user)): result = await db.execute( select(SRRequest.id, SRRequest.title, SRRequest.description, SRRequest.created_at) .where(SRRequest.status == "PENDING_APPROVAL") .limit(20) ) rows = result.all() docs = [{"id": r.id, "title": r.title, "content": r.description, "requester_name": "시스템", "created_at": str(r.created_at)} for r in rows] return {"docs": docs} @esign_router.post("/{doc_id}/sign") async def sign_document(doc_id: int, req: SignReq, _=Depends(get_current_user)): if not req.pin_hash or len(req.pin_hash) < 4: raise HTTPException(status_code=400, detail="PIN이 너무 짧습니다.") return {"signed": True, "doc_id": doc_id, "signed_at": datetime.utcnow().isoformat()}