CMDB 자동 발견 (4개): - autodiscovery.py: SSH 네트워크 스캔 + CMDB 자동 등록 - snmp_discovery.py: SNMP v2c/v3 장비 자동 발견 - dependency_map.py: 서비스 의존성 자동 매핑 (netstat) - config_inventory.py: 서버 인벤토리 자동 수집 (SSH) NL 쿼리 엔진 (3개): - nlquery.py: Text-to-SQL (SELECT 전용, DML 차단) - op_assistant.py: Multi-turn 대화형 운영 어시스턴트 - query_history.py: 쿼리 이력·즐겨찾기·공유 구성 드리프트 (3개): - drift_detection.py: 골든 구성 vs 실제 비교·SR 자동 생성 - golden_config.py: 내장 CSAP 템플릿 + 버전 관리 - auto_remediation.py: 승인 기반 자동 교정 + 롤백 멀티클라우드 (4개): - multicloud.py: 통합 관제 (NCloud+AWS+KT) - aws_connector.py: AWS SigV4 직접 서명 연동 - cost_optimizer.py: AI 비용 최적화 권고 - cloud_migration.py: On-prem→K-Cloud 체크리스트 공공기관 특화 (6개): - narasajang.py: 나라장터 OpenAPI 연동 - public_api_hub.py: data.go.kr KISA·기상청 허브 - isp_support.py: ISP 수립 지원 + AI 보고서 - network_zone.py: 행정망/인터넷망 분리 관리 - k_cloud.py: 정부 K-Cloud 전환 자동화 - e_procurement.py: 전자조달 계약·검수·납품 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
176 lines
4.9 KiB
Python
176 lines
4.9 KiB
Python
"""
|
|
쿼리 이력·즐겨찾기·공유 대시보드
|
|
|
|
운영자가 자주 쓰는 쿼리를 저장하고 팀과 공유.
|
|
|
|
엔드포인트:
|
|
GET /api/queryhistory/ — 내 쿼리 이력
|
|
GET /api/queryhistory/favorites — 즐겨찾기
|
|
POST /api/queryhistory/{id}/favorite — 즐겨찾기 토글
|
|
POST /api/queryhistory/save — 쿼리 직접 저장
|
|
GET /api/queryhistory/shared — 팀 공유 쿼리
|
|
POST /api/queryhistory/{id}/share — 공유 토글
|
|
DELETE /api/queryhistory/{id} — 이력 삭제
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select, desc
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from database import get_db
|
|
from models import User, QueryHistory
|
|
|
|
router = APIRouter(prefix="/api/queryhistory", tags=["쿼리 이력"])
|
|
|
|
|
|
class SaveQueryRequest(BaseModel):
|
|
question: str
|
|
sql: str
|
|
description: Optional[str] = None
|
|
|
|
|
|
@router.get("/")
|
|
async def list_history(
|
|
limit: int = 50,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
rows = await db.execute(
|
|
select(QueryHistory).where(QueryHistory.user_id == user.id)
|
|
.order_by(desc(QueryHistory.executed_at)).limit(limit)
|
|
)
|
|
hs = rows.scalars().all()
|
|
return [
|
|
{"id": h.id, "question": h.question, "sql": h.generated_sql,
|
|
"rows": h.row_count, "is_favorite": h.is_favorite,
|
|
"is_shared": h.is_shared, "executed_at": h.executed_at}
|
|
for h in hs
|
|
]
|
|
|
|
|
|
@router.get("/favorites")
|
|
async def list_favorites(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
rows = await db.execute(
|
|
select(QueryHistory).where(
|
|
QueryHistory.user_id == user.id,
|
|
QueryHistory.is_favorite == True,
|
|
).order_by(desc(QueryHistory.executed_at))
|
|
)
|
|
hs = rows.scalars().all()
|
|
return [
|
|
{"id": h.id, "question": h.question, "sql": h.generated_sql,
|
|
"description": h.description, "executed_at": h.executed_at}
|
|
for h in hs
|
|
]
|
|
|
|
|
|
@router.get("/shared")
|
|
async def list_shared(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""팀 공유 쿼리 (모든 테넌트 사용자)."""
|
|
rows = await db.execute(
|
|
select(QueryHistory, User.name.label("owner")).join(
|
|
User, QueryHistory.user_id == User.id
|
|
).where(
|
|
QueryHistory.is_shared == True,
|
|
).order_by(desc(QueryHistory.executed_at)).limit(100)
|
|
)
|
|
return [
|
|
{"id": r.QueryHistory.id, "question": r.QueryHistory.question,
|
|
"sql": r.QueryHistory.generated_sql, "description": r.QueryHistory.description,
|
|
"owner": r.owner, "executed_at": r.QueryHistory.executed_at}
|
|
for r in rows.all()
|
|
]
|
|
|
|
|
|
@router.post("/save")
|
|
async def save_query(
|
|
req: SaveQueryRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
h = QueryHistory(
|
|
user_id=user.id,
|
|
question=req.question,
|
|
generated_sql=req.sql,
|
|
description=req.description,
|
|
row_count=0,
|
|
is_favorite=True,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
db.add(h)
|
|
await db.commit()
|
|
await db.refresh(h)
|
|
return {"ok": True, "id": h.id}
|
|
|
|
|
|
@router.post("/{history_id}/favorite")
|
|
async def toggle_favorite(
|
|
history_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
row = await db.execute(
|
|
select(QueryHistory).where(
|
|
QueryHistory.id == history_id,
|
|
QueryHistory.user_id == user.id,
|
|
)
|
|
)
|
|
h = row.scalar_one_or_none()
|
|
if not h:
|
|
raise HTTPException(404)
|
|
h.is_favorite = not h.is_favorite
|
|
await db.commit()
|
|
return {"ok": True, "is_favorite": h.is_favorite}
|
|
|
|
|
|
@router.post("/{history_id}/share")
|
|
async def toggle_share(
|
|
history_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
row = await db.execute(
|
|
select(QueryHistory).where(
|
|
QueryHistory.id == history_id,
|
|
QueryHistory.user_id == user.id,
|
|
)
|
|
)
|
|
h = row.scalar_one_or_none()
|
|
if not h:
|
|
raise HTTPException(404)
|
|
h.is_shared = not h.is_shared
|
|
await db.commit()
|
|
return {"ok": True, "is_shared": h.is_shared}
|
|
|
|
|
|
@router.delete("/{history_id}")
|
|
async def delete_history(
|
|
history_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
row = await db.execute(
|
|
select(QueryHistory).where(
|
|
QueryHistory.id == history_id,
|
|
QueryHistory.user_id == user.id,
|
|
)
|
|
)
|
|
h = row.scalar_one_or_none()
|
|
if not h:
|
|
raise HTTPException(404)
|
|
await db.delete(h)
|
|
await db.commit()
|
|
return {"ok": True}
|