545 lines
28 KiB
Python
545 lines
28 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": "특이 사항 없음",
|
|
}
|
|
|
|
|
|
# ── 업무 캘린더 (#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()}
|