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>
109 lines
4.3 KiB
Python
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}
|