zioinfo-mail/workspace/guardia-itsm/routers/narasajang.py
DESKTOP-TKLFCPR\ython b8faec44e0 feat(advanced): GUARDiA 고급 확장 구현 — 20 routers + 754 endpoints
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>
2026-06-02 14:33:41 +09:00

109 lines
4.3 KiB
Python

"""
나라장터 연동 — 조달청 OpenAPI
공공기관 조달·계약·납품 이력을 ITSM과 연동.
엔드포인트:
POST /api/narasajang/config — API Key 설정
GET /api/narasajang/bids — 입찰 공고 조회
GET /api/narasajang/contracts — 계약 현황
POST /api/narasajang/link-sr — 계약 → SR 연계
GET /api/narasajang/procurement — 전자조달 이력
"""
from __future__ import annotations
import logging
from datetime import datetime
from typing import Optional
import httpx
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, require_admin_role
from database import get_db
from models import User, NarasajangConfig, ProcurementRecord
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/narasajang", tags=["나라장터 연동"])
NARA_API = "https://apis.data.go.kr/1230000"
class NaraConfigCreate(BaseModel):
api_key: str
institution_code: Optional[str] = None
async def _nara_request(api_key: str, path: str, params: dict = None) -> Optional[dict]:
params = {**(params or {}), "serviceKey": api_key, "numOfRows": 20, "pageNo": 1, "type": "json"}
try:
async with httpx.AsyncClient(timeout=15) as c:
r = await c.get(f"{NARA_API}/{path}", params=params)
return r.json() if r.status_code == 200 else None
except Exception as e:
logger.error(f"나라장터 API 실패: {e}")
return None
@router.post("/config")
async def save_config(req: NaraConfigCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role)):
row = await db.execute(select(NarasajangConfig).where(NarasajangConfig.tenant_id == user.tenant_id))
cfg = row.scalar_one_or_none()
if cfg:
cfg.api_key_enc = req.api_key; cfg.institution_code = req.institution_code
else:
cfg = NarasajangConfig(tenant_id=user.tenant_id, api_key_enc=req.api_key,
institution_code=req.institution_code, is_active=True,
created_at=datetime.utcnow())
db.add(cfg)
await db.commit()
return {"ok": True}
async def _get_cfg(user: User, db: AsyncSession):
row = await db.execute(select(NarasajangConfig).where(NarasajangConfig.tenant_id == user.tenant_id, NarasajangConfig.is_active == True))
cfg = row.scalar_one_or_none()
if not cfg: raise HTTPException(404, "나라장터 API Key 설정 필요")
return cfg
@router.get("/bids")
async def list_bids(q: str = None, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
cfg = await _get_cfg(user, db)
params = {}
if cfg.institution_code: params["dminsttId"] = cfg.institution_code
if q: params["bidNtceNm"] = q
data = await _nara_request(cfg.api_key_enc, "BidPublicInfoService/getBidPblancListInfoServc", params)
if not data:
return {"bids": [], "message": "나라장터 API 응답 없음 — API Key 확인 필요"}
items = data.get("response", {}).get("body", {}).get("items", [])
return {"bids": items[:20], "total": len(items)}
@router.get("/contracts")
async def list_contracts(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
rows = await db.execute(
select(ProcurementRecord).where(ProcurementRecord.tenant_id == user.tenant_id)
.order_by(desc(ProcurementRecord.end_date)).limit(50)
)
records = rows.scalars().all()
return [
{"id": r.id, "contract_no": r.contract_no, "name": r.contract_name,
"supplier": r.supplier, "amount": r.amount,
"start": r.start_date, "end": r.end_date, "status": r.status}
for r in records
]
@router.post("/link-sr/{contract_id}")
async def link_sr(contract_id: int, sr_ids: list[int], db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
row = await db.execute(select(ProcurementRecord).where(ProcurementRecord.id == contract_id, ProcurementRecord.tenant_id == user.tenant_id))
record = row.scalar_one_or_none()
if not record: raise HTTPException(404)
record.linked_sr_ids = sr_ids
await db.commit()
return {"ok": True, "linked_sr_ids": sr_ids}