""" 나라장터 연동 — 조달청 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}