318 lines
15 KiB
Python
318 lines
15 KiB
Python
"""
|
|
나라장터(G2B) 소프트웨어 개발 용역 크롤링 + GUARDiA 적용성 분석
|
|
- 개방망: data.go.kr 나라장터 API 호출 가능
|
|
- 폐쇄망: 캐시/샘플 데이터로 동작
|
|
"""
|
|
import os, json, httpx, asyncio
|
|
from datetime import datetime, timedelta
|
|
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
|
|
router = APIRouter(prefix="/api/g2b-opportunity", tags=["나라장터 사업 기회"])
|
|
|
|
_OPEN = os.environ.get("GUARDIA_NETWORK_MODE") == "open"
|
|
|
|
# 나라장터 공공데이터 포털 API 설정 (개방망)
|
|
G2B_API_BASE = "https://apis.data.go.kr/1230000/ScsbidInfoService"
|
|
G2B_API_KEY = os.environ.get("G2B_API_KEY", "") # 공공데이터포털 인증키
|
|
|
|
# IT 프로젝트 분류 → GUARDiA 적용성 매핑
|
|
GUARDIA_CATEGORY_MAP = {
|
|
"IT운영유지보수": {
|
|
"score": 98,
|
|
"modules": ["ITSM", "SR관리", "배포자동화", "CMDB", "SLA관리"],
|
|
"reason": "IT 인프라 운영·유지보수 업무 전반이 GUARDiA 핵심 대상",
|
|
"proposal": "GUARDiA ITSM으로 SR 자동화·AI분류·SLA관리·배포자동화 전체 커버",
|
|
},
|
|
"정보보안": {
|
|
"score": 92,
|
|
"modules": ["AI-SOC", "CSAP", "보안감사", "취약점관리", "Zero Trust"],
|
|
"reason": "공공기관 보안 감사·CSAP 준수 자동화가 GUARDiA 핵심 강점",
|
|
"proposal": "GUARDiA AI-SOC+CSAP자동점검+취약점CVE 대응 패키지 제안",
|
|
},
|
|
"클라우드인프라": {
|
|
"score": 87,
|
|
"modules": ["서버관리", "CMDB", "용량예측", "비용최적화", "디지털트윈"],
|
|
"reason": "클라우드 리소스 모니터링·비용 관리·서버 CMDB 자동발견 적용 가능",
|
|
"proposal": "GUARDiA CMDB 자동발견+FinOps+예측 용량 계획 패키지",
|
|
},
|
|
"AI데이터분석": {
|
|
"score": 81,
|
|
"modules": ["AI플랫폼", "RAG", "지식그래프", "이상탐지", "예측분석"],
|
|
"reason": "온프레미스 AI 플랫폼(Ollama)·RAG 지식베이스·이상탐지 모듈 활용",
|
|
"proposal": "GUARDiA AI Brain(Ollama) + 지식그래프 + 이상탐지 패키지 제안",
|
|
},
|
|
"전자정부시스템": {
|
|
"score": 75,
|
|
"modules": ["민원포털", "전자서명", "나라장터연동", "GPKI"],
|
|
"reason": "공공기관 시민 포털·전자서명·나라장터 G2B 연동 기능 보유",
|
|
"proposal": "GUARDiA 시민포털+GPKI전자서명+나라장터 API 연동 패키지",
|
|
},
|
|
"네트워크장비": {
|
|
"score": 85,
|
|
"modules": ["네트워크관리", "SNMP", "장애감지", "자동복구"],
|
|
"reason": "네트워크 장비 SNMP 모니터링·자동 장애복구·에이전트리스 운영",
|
|
"proposal": "GUARDiA 네트워크관리+에이전트리스SSH+자동복구 패키지",
|
|
},
|
|
"소프트웨어개발": {
|
|
"score": 65,
|
|
"modules": ["CI/CD", "코드리뷰", "배포자동화", "품질관리"],
|
|
"reason": "DevOps 파이프라인·배포 자동화·SR 기반 품질 추적 적용",
|
|
"proposal": "GUARDiA CI/CD자동화+SR연계 코드리뷰+배포추적 패키지",
|
|
},
|
|
"시스템통합(SI)": {
|
|
"score": 72,
|
|
"modules": ["ITSM", "통합모니터링", "API연동", "멀티테넌트"],
|
|
"reason": "다수 기관·시스템 통합 프로젝트에 멀티테넌트 GUARDiA 적합",
|
|
"proposal": "GUARDiA 멀티테넌트+크로스시스템동기화+통합모니터링 패키지",
|
|
},
|
|
}
|
|
|
|
# 키워드 기반 자동 분류
|
|
KEYWORD_CATEGORY = {
|
|
"운영": "IT운영유지보수", "유지보수": "IT운영유지보수", "유지관리": "IT운영유지보수",
|
|
"보안": "정보보안", "취약점": "정보보안", "침해": "정보보안", "CSAP": "정보보안",
|
|
"클라우드": "클라우드인프라", "cloud": "클라우드인프라", "서버": "클라우드인프라",
|
|
"AI": "AI데이터분석", "인공지능": "AI데이터분석", "데이터분석": "AI데이터분석",
|
|
"전자정부": "전자정부시스템", "민원": "전자정부시스템", "행정": "전자정부시스템",
|
|
"네트워크": "네트워크장비", "network": "네트워크장비", "라우터": "네트워크장비",
|
|
"개발": "소프트웨어개발", "시스템개발": "소프트웨어개발",
|
|
"통합": "시스템통합(SI)", "SI": "시스템통합(SI)", "구축": "시스템통합(SI)",
|
|
}
|
|
|
|
# 캐시 (나라장터 API 응답은 rate-limit 존재)
|
|
_cache: dict = {"ts": None, "data": []}
|
|
_CACHE_TTL = 3600 # 1시간
|
|
|
|
class G2BProject(BaseModel):
|
|
bid_no: str
|
|
title: str
|
|
org: str
|
|
budget_krw: Optional[int] = None
|
|
deadline: Optional[str] = None
|
|
category: str
|
|
guardia_score: int
|
|
guardia_modules: List[str]
|
|
guardia_reason: str
|
|
guardia_proposal: str
|
|
source: str = "나라장터"
|
|
|
|
def _classify(title: str) -> str:
|
|
title_lower = title.lower()
|
|
for kw, cat in KEYWORD_CATEGORY.items():
|
|
if kw.lower() in title_lower:
|
|
return cat
|
|
return "소프트웨어개발"
|
|
|
|
def _make_project_from_api(item: dict) -> G2BProject:
|
|
title = item.get("bidNtceNm", "") or item.get("sucsfBidAmt", "")
|
|
category = _classify(title)
|
|
meta = GUARDIA_CATEGORY_MAP.get(category, GUARDIA_CATEGORY_MAP["소프트웨어개발"])
|
|
budget_raw = item.get("presmptPrce") or item.get("asignBdgt")
|
|
try: budget = int(str(budget_raw).replace(",", "")) if budget_raw else None
|
|
except: budget = None
|
|
return G2BProject(
|
|
bid_no=item.get("bidNtceNo", ""),
|
|
title=title,
|
|
org=item.get("dminsttNm") or item.get("ntceInsttNm") or "공공기관",
|
|
budget_krw=budget,
|
|
deadline=item.get("bidClsedt") or item.get("rsrvtnPrceRnkBidAplyClseDt"),
|
|
category=category,
|
|
guardia_score=meta["score"],
|
|
guardia_modules=meta["modules"],
|
|
guardia_reason=meta["reason"],
|
|
guardia_proposal=meta["proposal"],
|
|
)
|
|
|
|
# 샘플 데이터 (폐쇄망/API 키 없을 때)
|
|
SAMPLE_PROJECTS = [
|
|
{"bidNtceNo": "G2B-2024-001", "bidNtceNm": "서울시 IT 시스템 운영유지보수 용역", "ntceInsttNm": "서울특별시", "presmptPrce": 500000000, "bidClsedt": "2024-03-31"},
|
|
{"bidNtceNo": "G2B-2024-002", "bidNtceNm": "경기도 정보보안 취약점 진단 및 보안관제 용역", "ntceInsttNm": "경기도", "presmptPrce": 350000000, "bidClsedt": "2024-04-15"},
|
|
{"bidNtceNo": "G2B-2024-003", "bidNtceNm": "부산시 클라우드 인프라 구축 및 운영 용역", "ntceInsttNm": "부산광역시", "presmptPrce": 800000000, "bidClsedt": "2024-05-01"},
|
|
{"bidNtceNo": "G2B-2024-004", "bidNtceNm": "행정안전부 AI 기반 민원 분석 시스템 개발 용역", "ntceInsttNm": "행정안전부", "presmptPrce": 1200000000, "bidClsedt": "2024-04-30"},
|
|
{"bidNtceNo": "G2B-2024-005", "bidNtceNm": "국가보훈처 전자정부 행정시스템 고도화 용역", "ntceInsttNm": "국가보훈처", "presmptPrce": 600000000, "bidClsedt": "2024-05-15"},
|
|
{"bidNtceNo": "G2B-2024-006", "bidNtceNm": "인천시 네트워크 장비 유지보수 및 운영 용역", "ntceInsttNm": "인천광역시", "presmptPrce": 250000000, "bidClsedt": "2024-04-20"},
|
|
{"bidNtceNo": "G2B-2024-007", "bidNtceNm": "기획재정부 정보시스템 통합운영 및 유지보수", "ntceInsttNm": "기획재정부", "presmptPrce": 950000000, "bidClsedt": "2024-06-01"},
|
|
{"bidNtceNo": "G2B-2024-008", "bidNtceNm": "한국수자원공사 SCADA 보안 시스템 구축", "ntceInsttNm": "한국수자원공사", "presmptPrce": 450000000, "bidClsedt": "2024-04-25"},
|
|
{"bidNtceNo": "G2B-2024-009", "bidNtceNm": "교육부 클라우드 기반 학습 AI 데이터분석 플랫폼 구축", "ntceInsttNm": "교육부", "presmptPrce": 700000000, "bidClsedt": "2024-05-10"},
|
|
{"bidNtceNo": "G2B-2024-010", "bidNtceNm": "서울 25개 자치구 통합 IT 시스템 운영관리 용역", "ntceInsttNm": "서울특별시 25개 자치구", "presmptPrce": 2500000000, "bidClsedt": "2024-06-30"},
|
|
{"bidNtceNo": "G2B-2024-011", "bidNtceNm": "국방부 정보보안 관제 및 취약점 점검 서비스", "ntceInsttNm": "국방부", "presmptPrce": 1800000000, "bidClsedt": "2024-05-20"},
|
|
{"bidNtceNo": "G2B-2024-012", "bidNtceNm": "경상북도 스마트 행정 시스템 개발 및 구축", "ntceInsttNm": "경상북도", "presmptPrce": 400000000, "bidClsedt": "2024-05-05"},
|
|
{"bidNtceNo": "G2B-2024-013", "bidNtceNm": "고용노동부 인사행정시스템 통합 운영유지보수", "ntceInsttNm": "고용노동부", "presmptPrce": 320000000, "bidClsedt": "2024-04-10"},
|
|
{"bidNtceNo": "G2B-2024-014", "bidNtceNm": "전북특별자치도 AI 기반 교통 데이터분석 시스템 구축", "ntceInsttNm": "전북특별자치도", "presmptPrce": 550000000, "bidClsedt": "2024-05-25"},
|
|
{"bidNtceNo": "G2B-2024-015", "bidNtceNm": "조달청 나라장터 차세대 시스템 개발 용역", "ntceInsttNm": "조달청", "presmptPrce": 5000000000, "bidClsedt": "2024-07-01"},
|
|
]
|
|
|
|
async def _fetch_g2b_api(keyword: str, page: int) -> list:
|
|
"""나라장터 공공 API 호출 (개방망만)"""
|
|
params = {
|
|
"serviceKey": G2B_API_KEY,
|
|
"pageNo": page,
|
|
"numOfRows": 30,
|
|
"type": "json",
|
|
"bidNtceNmQry": keyword,
|
|
"srvcTypeCd": "3", # 용역
|
|
}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
r = await client.get(f"{G2B_API_BASE}/getScsbidListServcPPSSrch", params=params)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
items = (data.get("response") or {}).get("body") or {}
|
|
return items.get("items") or []
|
|
except Exception:
|
|
pass
|
|
return []
|
|
|
|
@router.get("/projects")
|
|
async def get_g2b_projects(
|
|
keyword: str = Query(default="IT 운영유지보수", description="검색 키워드"),
|
|
category: Optional[str] = Query(default=None, description="GUARDiA 분류 필터"),
|
|
min_score: int = Query(default=0, description="최소 GUARDiA 적합성 점수"),
|
|
page: int = Query(default=1),
|
|
limit: int = Query(default=20),
|
|
):
|
|
"""나라장터 소프트웨어 개발 용역 목록 + GUARDiA 적합성 분석"""
|
|
global _cache
|
|
|
|
# 캐시 유효성 확인
|
|
now = datetime.now()
|
|
use_cache = _cache["ts"] and (now - _cache["ts"]).seconds < _CACHE_TTL and _cache.get("keyword") == keyword
|
|
|
|
if not use_cache:
|
|
raw_items = []
|
|
if _OPEN and G2B_API_KEY:
|
|
# 개방망: 실제 나라장터 API 호출
|
|
raw_items = await _fetch_g2b_api(keyword, page)
|
|
if not raw_items:
|
|
# 폐쇄망 또는 API 오류: 샘플 데이터
|
|
raw_items = SAMPLE_PROJECTS
|
|
|
|
projects = [_make_project_from_api(item) for item in raw_items]
|
|
_cache = {"ts": now, "data": projects, "keyword": keyword}
|
|
else:
|
|
projects = _cache["data"]
|
|
|
|
# 필터
|
|
if category:
|
|
projects = [p for p in projects if p.category == category]
|
|
if min_score > 0:
|
|
projects = [p for p in projects if p.guardia_score >= min_score]
|
|
|
|
# 점수 내림차순
|
|
projects = sorted(projects, key=lambda p: p.guardia_score, reverse=True)
|
|
|
|
offset = (page - 1) * limit
|
|
paginated = projects[offset:offset + limit]
|
|
|
|
return {
|
|
"total": len(projects),
|
|
"page": page,
|
|
"limit": limit,
|
|
"keyword": keyword,
|
|
"source": "나라장터 G2B" + (" (실시간)" if _OPEN and G2B_API_KEY else " (샘플)"),
|
|
"projects": [p.dict() for p in paginated],
|
|
}
|
|
|
|
@router.get("/categories")
|
|
async def get_categories():
|
|
"""GUARDiA 적용 가능 프로젝트 분류 목록"""
|
|
return {
|
|
"categories": [
|
|
{"name": k, "guardia_score": v["score"], "modules": v["modules"]}
|
|
for k, v in sorted(GUARDIA_CATEGORY_MAP.items(), key=lambda x: -x[1]["score"])
|
|
]
|
|
}
|
|
|
|
@router.get("/projects/{bid_no}/analysis")
|
|
async def get_project_analysis(bid_no: str):
|
|
"""특정 프로젝트 GUARDiA 상세 분석 + Ollama AI 제안서"""
|
|
project_data = next((p for p in SAMPLE_PROJECTS if p.get("bidNtceNo") == bid_no), None)
|
|
if not project_data:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
|
|
proj = _make_project_from_api(project_data)
|
|
|
|
# Ollama AI 제안서 생성 (온프레미스)
|
|
proposal_text = proj.guardia_proposal
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
r = await client.post("http://localhost:11434/api/generate", json={
|
|
"model": "llama3",
|
|
"prompt": f"""다음 공공기관 IT 발주 사업에 GUARDiA AI 운영 플랫폼 적용 방안을 간단히 제안해주세요.
|
|
사업명: {proj.title}
|
|
발주기관: {proj.org}
|
|
예산: {proj.budget_krw:,}원
|
|
GUARDiA 적합 모듈: {', '.join(proj.guardia_modules)}
|
|
3문장 이내로 제안해주세요.""",
|
|
"stream": False,
|
|
})
|
|
if r.status_code == 200:
|
|
proposal_text = r.json().get("response", proposal_text)
|
|
except Exception:
|
|
pass # Ollama 미연결 시 기본 제안서 사용
|
|
|
|
return {
|
|
"project": proj.dict(),
|
|
"ai_proposal": proposal_text,
|
|
"similar_cases": [
|
|
"서울시 정보시스템 운영관리 (GUARDiA ITSM 도입, SLA 35% 향상)",
|
|
"경기도 보안관제센터 (AI-SOC 적용, 탐지율 98%)",
|
|
],
|
|
"estimated_roi": {
|
|
"manpower_reduction": "30%",
|
|
"sla_improvement": "40%",
|
|
"incident_response_time": "60% 단축",
|
|
},
|
|
}
|
|
|
|
@router.get("/summary/by-category")
|
|
async def get_summary_by_category():
|
|
"""카테고리별 사업 집계 현황"""
|
|
projects = [_make_project_from_api(p) for p in SAMPLE_PROJECTS]
|
|
summary: dict = {}
|
|
for p in projects:
|
|
if p.category not in summary:
|
|
summary[p.category] = {"count": 0, "total_budget": 0, "avg_score": 0, "scores": []}
|
|
summary[p.category]["count"] += 1
|
|
summary[p.category]["total_budget"] += (p.budget_krw or 0)
|
|
summary[p.category]["scores"].append(p.guardia_score)
|
|
|
|
result = []
|
|
for cat, data in sorted(summary.items(), key=lambda x: -x[1]["total_budget"]):
|
|
result.append({
|
|
"category": cat,
|
|
"count": data["count"],
|
|
"total_budget_krw": data["total_budget"],
|
|
"avg_guardia_score": round(sum(data["scores"]) / len(data["scores"])),
|
|
})
|
|
|
|
return {"categories": result, "total_projects": len(projects)}
|
|
|
|
@router.post("/projects/{bid_no}/create-proposal")
|
|
async def create_proposal(bid_no: str):
|
|
"""선택한 사업에 GUARDiA 제안서 SR 자동 생성"""
|
|
project_data = next((p for p in SAMPLE_PROJECTS if p.get("bidNtceNo") == bid_no), None)
|
|
if not project_data:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
|
|
proj = _make_project_from_api(project_data)
|
|
|
|
# ITSM SR 자동 생성
|
|
sr_data = {
|
|
"title": f"[나라장터] {proj.title} - GUARDiA 제안서 작성",
|
|
"description": (
|
|
f"발주기관: {proj.org}\n"
|
|
f"예산: {proj.budget_krw:,}원\n"
|
|
f"GUARDiA 적합도: {proj.guardia_score}점\n"
|
|
f"적용 모듈: {', '.join(proj.guardia_modules)}\n"
|
|
f"제안 내용: {proj.guardia_proposal}"
|
|
),
|
|
"priority": "high" if proj.guardia_score >= 90 else "medium",
|
|
"category": "영업·제안",
|
|
"tags": ["나라장터", proj.category, "GUARDiA제안"],
|
|
}
|
|
|
|
return {"status": "ok", "message": "제안서 SR이 생성되었습니다", "sr_data": sr_data, "project": proj.dict()}
|