스페이스락(SpaceRock) 전용 렌탈 영업 인텔리전스 스킬 — 대형 텐트·오버레이 임대 관련 입찰 공고를 나라장터(G2B) API + 다중 공공기관 사이트 웹 크롤링으로 자동 수집·분석하고, 사업명/입찰품목/사업금액/제출시기/입찰시기가 포함된 HTML 보고서를 생성한다. 매일 오전 10시 자동 실행 (Zapier 연동). 반드시 이 스킬을 사용해야 하는 상황: - "RENT", "렌탈", "렌탈 스킬", "rent 스킬" 직접 언급 시 - 나라장터, 입찰공고, 조달청, G2B 관련 검색·분석 요청 시 - 텐트, 오버레이, 천막, 임시구조물, 대형텐트 입찰 수집 요청 시 - "스페이스락 관련 입찰 찾아줘", "이번 주 입찰 공고 정리해줘" 패턴 - 발주처 분석, 사업 규모 분석, 입찰 대응 전략 요청 시 - 대표·직원에게 입찰 정보 이메일 공유 요청 시 - "Q야", "영업 브리핑", "이번 달 입찰 현황" 언급 시 - 민간 기업 이벤트·행사 텐트 렌탈 수주 가능성 분석 요청 시
담당자: Q (스페이스락 마케팅 영업)
회사: 스페이스락 (www.spacerock.kr) — 대한민국 최고 대형 텐트·오버레이 임대 전문기업
자동화 주기: 매일 오전 10:00 KST
스페이스락 업종(대형 천막·오버레이·임시구조물 렌탈)과 관련된 모든 공공조달 입찰 공고를 매일 오전 10시에 자동 수집·분석하여 HTML 보고서로 생성하고, 지정 이메일로 발송한다.
| 기능 | 설명 |
|---|---|
| 📡 G2B API 수집 | 나라장터 공공데이터 API — 주요 소스 (필수) |
| 🕷️ 다중 사이트 크롤링 | S2B, 벤처나라, 지자체, D2B 등 추가 수집 |
| 🔍 AI 분석 | 연관성 점수화 + 5대 필수항목 자동 추출 |
| 📊 HTML 보고서 | 5대 필수항목 완전 포함, GitHub Pages 배포 |
| ⏰ 일일 자동화 | 매일 10:00 KST Zapier → Python 실행 |
| 📧 이메일 공유 | 분석 보고서 지정 수신자 자동 발송 |
1. 사업명 — 입찰 공고의 정식 사업명
2. 입찰 공고 품목 — 조달 품목 분류 및 상세 내용
3. 사업 금액 — 추정가격 / 기초금액 (원 단위 명시)
4. 제출 시기 — 서류·제안서 제출 마감일
5. 입찰 시기 — 입찰서 제출(등록) 마감일 / 개찰일
| 사이트 | URL | 접근 방법 | API 인증 | 비고 |
|---|---|---|---|---|
| 나라장터(G2B) | g2b.go.kr | ✅ 공공데이터 API | 필요 | 공공데이터포털 인증키 |
| 학교장터(S2B) | s2b.kr | 🕷️ 직접 크롤링 | 불필요 | POST 검색, BeautifulSoup |
| 벤처나라 | venturena.go.kr | 🕷️ 직접 크롤링 | 불필요 | GET 검색 가능 |
| 나라장터 국방(D2B) | d2b.go.kr | 🕷️ 직접 크롤링 | 불필요 | 국방조달, 천막 수요 있음 |
| 지방계약정보 | lps.go.kr | 🕷️ 직접 크롤링 | 불필요 | 지자체 자체계약 |
| 서울특별시 전자조달 | seoulcontract.seoul.go.kr | 🕷️ 직접 크롤링 | 불필요 | 서울시 발주 |
| 경기도 전자조달 | ebid.gg.go.kr | 🕷️ 직접 크롤링 | 불필요 | 경기도 발주 |
| 조달청 혁신제품 | pps.go.kr | 🕷️ 직접 크롤링 | 불필요 | 혁신조달 품목 |
| 웹 검색 보완 | 구글/네이버 | 🔍 web_search | 불필요 | 누락 공고 보완 |
⚠️ 나라장터 직접 크롤링 불가 이유: g2b.go.kr은 세션 기반 로그인 + JavaScript 렌더링 필수. 반드시 공공데이터포털 API를 사용해야 한다.
# 1순위 (직접 연관 — 모든 사이트 필수 검색)
오버레이, overlay, 대형텐트, 텐트임대, 텐트렌탈, 천막임대, 천막렌탈
야외구조물, 임시구조물, 행사텐트, 이벤트텐트, 전시텐트, 파티텐트
측면막, 사이드월, 돔텐트, 돔구조물, 스트레치텐트, 에어텐트, 에어돔
# 2순위 (간접 연관)
행사용품임대, 행사장치, 행사설치, 의전천막, 의전시설
좌석임대, 관람석임대, 야외무대, 야외행사, 야외공연
우천대비시설, 우천막, 비가림막, 선셰이드, 차양막
가설건축물, 임시건축물, 조립식구조물, 모듈러구조물
# 3순위 (행사 유형 — 수요 발생)
축제, 행사, 박람회, 전시회, 경기, 시합, 체육대회
기업행사, 야외결혼식, 기념식, 준공식, 개막식, 폐막식
물품 품목: 56101 (천막·임시구조물)
43211500계열 (텐트류)
용역 업종: 73102 (이벤트·행사 기획)
80141600계열 (행사지원서비스)
공사: 건설업 - 가설구조물 관련
[10:00 KST] Zapier Schedule 트리거
↓
[STEP 1] G2B API 수집 (나라장터 — 필수)
↓
[STEP 2] 다중 사이트 크롤링 (S2B, 벤처나라, D2B, 지자체 등)
↓
[STEP 3] 웹 검색 보완 (web_search 도구)
↓
[STEP 4] 중복 제거 + AI 분석 + 5대 필수항목 추출
↓
[STEP 5] HTML 보고서 생성 + GitHub Pages 업로드
↓
[STEP 6] 이메일 발송
1. 공공데이터포털 접속: https://www.data.go.kr
2. 회원가입 → 로그인
3. 검색: "나라장터 입찰공고정보서비스"
→ 정식명: "국가종합전자조달 나라장터 입찰공고정보 서비스"
4. [활용신청] → 활용 목적: "렌탈업 영업 입찰 정보 수집"
5. 승인(즉시 또는 1~2일) → 마이페이지 → 인증키(UTF-8) 복사
6. Claude에게 전달: "내 G2B API 키는 [키값]이야"
※ API 호출 한도: 일 1,000건 (무료), 초과 시 유료 신청 가능
※ 서비스명 여러 개 있음 — 아래 엔드포인트 참고
베이스 URL: http://apis.data.go.kr/1230000/BidPublicInfoService04
# 용역 입찰공고 목록
GET /getBidPblancListInfoServc04
# 물품 입찰공고 목록
GET /getBidPblancListInfoThng04
# 공사 입찰공고 목록
GET /getBidPblancListInfoCnstwk04
# 입찰공고 상세 (공고번호로 전문 조회 — 5대 필수항목 추출에 활용)
GET /getBidPblancListInfoServcDtl04 ← 용역 상세
GET /getBidPblancListInfoThngDtl04 ← 물품 상세
# scripts/g2b_collector.py
import requests, json, urllib.parse
from datetime import datetime, timedelta
G2B_API_KEY = "YOUR_API_KEY" # 공공데이터포털 발급 키 (UTF-8)
BASE = "http://apis.data.go.kr/1230000/BidPublicInfoService04"
KEYWORDS = [
"오버레이", "텐트", "천막", "임시구조물", "대형텐트",
"행사텐트", "이벤트텐트", "전시텐트", "야외구조물",
"비가림막", "차양막", "에어돔", "가설건축물"
]
ENDPOINTS = {
"용역": f"{BASE}/getBidPblancListInfoServc04",
"물품": f"{BASE}/getBidPblancListInfoThng04",
"공사": f"{BASE}/getBidPblancListInfoCnstwk04",
}
DETAIL_ENDPOINTS = {
"용역": f"{BASE}/getBidPblancListInfoServcDtl04",
"물품": f"{BASE}/getBidPblancListInfoThngDtl04",
}
def search_g2b(keyword, days_back=14):
"""나라장터 API로 키워드 검색"""
end_dt = datetime.now()
start_dt = end_dt - timedelta(days=days_back)
results = []
for bid_type, url in ENDPOINTS.items():
params = {
"ServiceKey": G2B_API_KEY,
"pageNo": "1",
"numOfRows": "100",
"inqryDiv": "1",
"type": "json",
"bidNm": keyword,
"inqryBgnDt": start_dt.strftime("%Y%m%d0000"),
"inqryEndDt": end_dt.strftime("%Y%m%d2359"),
}
try:
r = requests.get(url, params=params, timeout=15)
data = r.json()
items = data.get("response", {}).get("body", {}).get("items", [])
if isinstance(items, dict):
items = [items]
for item in (items or []):
item["_type"] = bid_type
item["_keyword"] = keyword
results.extend(items or [])
except Exception as e:
print(f"[G2B ERROR] {bid_type} / {keyword}: {e}")
return results
def get_detail(bid_no, bid_type="용역"):
"""공고 상세 조회 — 5대 필수항목 전문 추출"""
url = DETAIL_ENDPOINTS.get(bid_type, DETAIL_ENDPOINTS["용역"])
params = {
"ServiceKey": G2B_API_KEY,
"pageNo": "1",
"numOfRows": "1",
"type": "json",
"bidNtceNo": bid_no,
}
try:
r = requests.get(url, params=params, timeout=15)
data = r.json()
items = data.get("response", {}).get("body", {}).get("items", {})
return items if isinstance(items, dict) else (items[0] if items else {})
except:
return {}
def extract_five_fields(item, detail=None):
"""5대 필수항목 추출"""
d = detail or {}
return {
"사업명": item.get("bidNm") or d.get("bidNm", "미확인"),
"입찰공고품목": (
item.get("ntceInsttNm") or
d.get("prdctClsfcNoNm") or
d.get("ntceInsttNm", "미확인")
),
"사업금액": (
item.get("asignBdgtAmt") or
d.get("asignBdgtAmt") or
item.get("presmptPrc", "미공개")
),
"제출시기": (
d.get("bidQlfctDocsMthdNm") or
item.get("bidClseDt", "미확인")
),
"입찰시기": {
"마감": item.get("bidClseDt", "미확인"),
"개찰": item.get("opengDt", "미확인"),
},
}
def collect_all_g2b():
"""전체 키워드 수집 + 중복 제거"""
all_bids = []
for kw in KEYWORDS:
all_bids.extend(search_g2b(kw))
seen = set()
unique = []
for bid in all_bids:
key = bid.get("bidNtceNo", "") + bid.get("bidNtceOrd", "")
if key and key not in seen:
seen.add(key)
unique.append(bid)
print(f"[G2B] 총 {len(unique)}건 수집 (중복 제거 후)")
return unique
# scripts/s2b_crawler.py
import requests
from bs4 import BeautifulSoup
S2B_SEARCH = "https://www.s2b.kr/s2bGov/bid/bidList.do"
def search_s2b(keyword):
"""학교장터 입찰 검색"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://www.s2b.kr",
}
payload = {
"searchWord": keyword,
"pageIndex": "1",
"bidType": "ALL",
}
results = []
try:
r = requests.post(S2B_SEARCH, data=payload, headers=headers, timeout=15)
soup = BeautifulSoup(r.text, "html.parser")
rows = soup.select("table.list tbody tr")
for row in rows:
cols = row.select("td")
if len(cols) >= 6:
results.append({
"source": "S2B(학교장터)",
"사업명": cols[1].get_text(strip=True),
"발주처": cols[2].get_text(strip=True),
"사업금액": cols[3].get_text(strip=True),
"입찰시기": cols[4].get_text(strip=True),
"제출시기": cols[5].get_text(strip=True),
"입찰공고품목": keyword,
"_keyword": keyword,
})
except Exception as e:
print(f"[S2B ERROR] {keyword}: {e}")
return results
# scripts/venture_crawler.py
import requests
from bs4 import BeautifulSoup
VENTURENA_URL = "https://www.venturena.or.kr/user/bids/retrieveBidList.do"
def search_venturena(keyword):
"""벤처나라 입찰 검색"""
headers = {"User-Agent": "Mozilla/5.0"}
params = {"searchWord": keyword, "pageIndex": 1}
results = []
try:
r = requests.get(VENTURENA_URL, params=params, headers=headers, timeout=15)
soup = BeautifulSoup(r.text, "html.parser")
rows = soup.select(".board-list tbody tr")
for row in rows:
cols = row.select("td")
if len(cols) >= 5:
results.append({
"source": "벤처나라",
"사업명": cols[0].get_text(strip=True),
"발주처": cols[1].get_text(strip=True),
"사업금액": cols[2].get_text(strip=True),
"입찰시기": cols[3].get_text(strip=True),
"제출시기": cols[3].get_text(strip=True),
"입찰공고품목": keyword,
"_keyword": keyword,
})
except Exception as e:
print(f"[벤처나라 ERROR] {keyword}: {e}")
return results
# scripts/lps_crawler.py
import requests
from bs4 import BeautifulSoup
LPS_URL = "https://www.lps.go.kr/co/search/searchNotice.do"
def search_lps(keyword):
"""지방계약정보(행안부) 입찰 검색"""
headers = {"User-Agent": "Mozilla/5.0"}
params = {
"searchWord": keyword,
"menuNo": "060100",
"pageIndex": "1",
}
results = []
try:
r = requests.get(LPS_URL, params=params, headers=headers, timeout=15)
soup = BeautifulSoup(r.text, "html.parser")
rows = soup.select("table tbody tr")
for row in rows:
cols = row.select("td")
if len(cols) >= 5:
results.append({
"source": "지방계약정보(LPS)",
"사업명": cols[1].get_text(strip=True),
"발주처": cols[2].get_text(strip=True),
"사업금액": cols[3].get_text(strip=True),
"입찰시기": cols[4].get_text(strip=True),
"제출시기": cols[4].get_text(strip=True),
"입찰공고품목": keyword,
"_keyword": keyword,
})
except Exception as e:
print(f"[LPS ERROR] {keyword}: {e}")
return results
# scripts/d2b_crawler.py
import requests
from bs4 import BeautifulSoup
D2B_URL = "https://www.d2b.go.kr/bid/bidListView.do"
def search_d2b(keyword):
"""국방전자조달 입찰 검색"""
headers = {"User-Agent": "Mozilla/5.0"}
params = {"searchWord": keyword, "pageIndex": 1}
results = []
try:
r = requests.get(D2B_URL, params=params, headers=headers, timeout=15)
soup = BeautifulSoup(r.text, "html.parser")
rows = soup.select("table.grid tbody tr")
for row in rows:
cols = row.select("td")
if len(cols) >= 5:
results.append({
"source": "국방전자조달(D2B)",
"사업명": cols[1].get_text(strip=True),
"발주처": cols[2].get_text(strip=True),
"사업금액": cols[3].get_text(strip=True),
"입찰시기": cols[4].get_text(strip=True),
"제출시기": cols[4].get_text(strip=True),
"입찰공고품목": keyword,
"_keyword": keyword,
})
except Exception as e:
print(f"[D2B ERROR] {keyword}: {e}")
return results
Claude가 직접 실행 시 아래 쿼리로 web_search 도구를 사용하여 누락 공고를 보완한다.
SEARCH_QUERIES = [
"나라장터 텐트 임대 입찰공고 2024",
"나라장터 오버레이 렌탈 입찰",
"나라장터 천막 행사 용역 입찰",
"나라장터 임시구조물 설치 용역",
"나라장터 야외구조물 렌탈 공고",
"g2b 텐트 천막 행사 입찰",
"공공기관 이벤트텐트 대관 렌탈 입찰",
"지자체 행사 천막 임대 입찰공고",
"축제 텐트 설치 운영 용역 입찰",
"박람회 전시 오버레이 구조물 입찰",
]
# Claude 실행 시: 위 쿼리 각각 web_search → 결과에서 입찰 공고 URL 추출
# → web_fetch로 공고 원문 접근 → 5대 필수항목 파싱
키워드 매칭 (30점):
- 오버레이/텐트/천막/임시구조물 포함: 30점
- 행사/이벤트/전시/박람회 포함: 20점
- 야외/임시/설치/렌탈 포함: 10점
사업 금액 (30점):
- 1억 이상: 30점
- 5천만~1억: 20점
- 1천만~5천만: 10점
- 1천만 미만: 5점
시기 적합성 (20점):
- 봄/가을(3~5월, 9~11월): 20점
- 여름/겨울: 10점
입찰 방식 (20점):
- 일반경쟁: 20점
- 제한경쟁: 15점
- 지명경쟁: 5점
rent_spacerock_bid_{YYYYMMDD}.html
예: rent_spacerock_bid_20260411.html
[헤더] 스페이스락 입찰 인텔리전스 리포트 — 수집일자
[KPI 카드] 신규공고수 / A등급수 / 총사업금액 / 마감임박수
[수집 소스 현황] G2B / S2B / 벤처나라 / D2B / 기타 (사이트별 수집 건수)
[메인 테이블] — 아래 5대 필수항목 반드시 컬럼 포함
┌─────────────────────────────────────────────────────────────────┐
│ 등급 │ 사업명 │ 입찰공고품목 │ 사업금액 │ 제출시기 │ 입찰시기 │
│ │ │ │ │ (마감) │ (개찰) │
│ 출처 │ 발주처 │ 지역 │ 연관점수 │ 공고링크 │ │
└─────────────────────────────────────────────────────────────────┘
[공고별 상세 요약 카드] — 각 A/B등급 건별 상세 블록
┌─ [등급배지] 사업명 ─────────────────────────────────────────┐
│ 📋 사업명: _______________________________________________ │
│ 🏷️ 입찰공고품목: ________________________________________ │
│ 💰 사업금액: ______________ 원 │
│ 📅 제출시기(마감): YYYY-MM-DD HH:MM │
│ 🔔 입찰시기(개찰): YYYY-MM-DD HH:MM │
│ 🏢 발주처: _______________ 📍 지역: ___________________ │
│ 📡 출처: G2B/S2B/벤처나라 🔗 공고 원문 링크 │
│ 📝 공고 요약: (AI 생성 1~3문장) │
└────────────────────────────────────────────────────────────┘
[투두리스트] A등급 건별 D-14~개찰 액션 플랜
[차트] 사업금액 버블차트, 발주처 파이, 마감일 타임라인
[하단] 수집시간, 출처별 통계, 연락처
# scripts/html_generator.py
from datetime import datetime
def generate_html_report(bids, date_str=None):
"""5대 필수항목 포함 HTML 보고서 생성"""
if not date_str:
date_str = datetime.now().strftime("%Y년 %m월 %d일")
# 등급별 분류
grade_a = [b for b in bids if b.get("score", 0) >= 80]
grade_b = [b for b in bids if 50 <= b.get("score", 0) < 80]
grade_c = [b for b in bids if b.get("score", 0) < 50]
# 총 사업금액 합산
total_amt = sum(
int(str(b.get("사업금액", "0")).replace(",", "").replace("원", "").strip() or 0)
for b in bids if b.get("사업금액")
)
rows_html = ""
for b in sorted(bids, key=lambda x: x.get("score", 0), reverse=True):
grade = "🔴A" if b.get("score", 0) >= 80 else ("🟡B" if b.get("score", 0) >= 50 else "🟢C")
bid_time = b.get("입찰시기", {})
if isinstance(bid_time, dict):
bid_deadline = bid_time.get("마감", "-")
bid_open = bid_time.get("개찰", "-")
else:
bid_deadline = bid_time
bid_open = "-"
rows_html += f"""
<tr>
<td><span class="badge grade-{grade[1]}">{grade}</span></td>
<td class="bid-name">{b.get('사업명', '-')}</td>
<td>{b.get('입찰공고품목', '-')}</td>
<td class="amount">{b.get('사업금액', '-')}</td>
<td>{b.get('제출시기', '-')}</td>
<td>{bid_deadline}</td>
<td>{bid_open}</td>
<td>{b.get('발주처', '-')}</td>
<td><span class="source">{b.get('source', 'G2B')}</span></td>
<td>{b.get('score', 0)}점</td>
</tr>"""
cards_html = ""
for b in grade_a + grade_b:
score = b.get("score", 0)
grade_cls = "A" if score >= 80 else "B"
bid_time = b.get("입찰시기", {})
if isinstance(bid_time, dict):
bid_deadline = bid_time.get("마감", "미확인")
bid_open = bid_time.get("개찰", "미확인")
else:
bid_deadline = str(bid_time)
bid_open = "미확인"
cards_html += f"""
<div class="bid-card grade-{grade_cls}-card">
<div class="card-header">
<span class="grade-badge-{grade_cls}">{"🔴 A등급 즉시대응" if grade_cls=="A" else "🟡 B등급 검토필요"}</span>
<span class="score-badge">{score}점</span>
</div>
<table class="detail-table">
<tr><td>📋 <strong>사업명</strong></td><td>{b.get('사업명','-')}</td></tr>
<tr><td>🏷️ <strong>입찰공고품목</strong></td><td>{b.get('입찰공고품목','-')}</td></tr>
<tr><td>💰 <strong>사업금액</strong></td><td><strong style="color:#e74c3c">{b.get('사업금액','-')}</strong></td></tr>
<tr><td>📅 <strong>제출시기(마감)</strong></td><td>{b.get('제출시기','-')}</td></tr>
<tr><td>🔔 <strong>입찰시기(개찰)</strong></td><td>{bid_open}</td></tr>
<tr><td>🏢 <strong>발주처</strong></td><td>{b.get('발주처','-')}</td></tr>
<tr><td>📡 <strong>출처</strong></td><td>{b.get('source','G2B')}</td></tr>
</table>
<div class="summary-box">📝 {b.get('summary','공고 내용을 확인하세요.')}</div>
</div>"""
html = f"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>스페이스락 입찰 인텔리전스 — {date_str}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: 'Noto Sans KR', Arial, sans-serif; background: #f0f4f8; color: #2d3748; }}
.header {{ background: linear-gradient(135deg, #1a365d, #2b6cb0); color: white; padding: 30px; text-align: center; }}
.header h1 {{ font-size: 26px; font-weight: 700; }}
.header p {{ margin-top: 8px; font-size: 14px; opacity: 0.85; }}
.container {{ max-width: 1400px; margin: 0 auto; padding: 20px; }}
.kpi-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin: 20px 0; }}
.kpi-card {{ background: white; border-radius: 12px; padding: 20px; text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.08); border-top: 4px solid #2b6cb0; }}
.kpi-value {{ font-size: 32px; font-weight: 700; color: #2b6cb0; }}
.kpi-label {{ font-size: 13px; color: #718096; margin-top: 4px; }}
.section {{ background: white; border-radius: 12px; padding: 24px; margin: 16px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.08); }}
.section-title {{ font-size: 18px; font-weight: 700; border-left: 4px solid #2b6cb0;
padding-left: 12px; margin-bottom: 16px; }}
table.main-table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
table.main-table th {{ background: #2b6cb0; color: white; padding: 10px 8px; text-align: center; white-space: nowrap; }}
table.main-table td {{ padding: 9px 8px; border-bottom: 1px solid #e2e8f0; vertical-align: top; }}
table.main-table tr:hover {{ background: #ebf8ff; }}
.badge {{ padding: 3px 8px; border-radius: 12px; font-weight: 700; font-size: 12px; }}
.badge.grade-A {{ background: #fff5f5; color: #c53030; border: 1px solid #fc8181; }}
.badge.grade-B {{ background: #fffff0; color: #b7791f; border: 1px solid #f6e05e; }}
.badge.grade-C {{ background: #f0fff4; color: #276749; border: 1px solid #68d391; }}
.amount {{ font-weight: 700; color: #2d3748; }}
.bid-name {{ font-weight: 600; }}
.source {{ background: #ebf8ff; color: #2b6cb0; padding: 2px 6px; border-radius: 8px; font-size: 11px; }}
.bid-card {{ border: 1px solid #e2e8f0; border-radius: 10px; padding: 18px; margin: 12px 0; }}
.bid-card.grade-A-card {{ border-left: 5px solid #fc8181; background: #fff5f5; }}
.bid-card.grade-B-card {{ border-left: 5px solid #f6e05e; background: #fffff0; }}
.card-header {{ display: flex; justify-content: space-between; margin-bottom: 12px; }}
.detail-table {{ width: 100%; font-size: 13px; }}
.detail-table td {{ padding: 5px 8px; }}
.detail-table td:first-child {{ width: 160px; color: #718096; }}
.summary-box {{ background: #f7fafc; border-radius: 6px; padding: 10px; margin-top: 10px;
font-size: 13px; color: #4a5568; border-left: 3px solid #90cdf4; }}
.source-grid {{ display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }}
.source-card {{ text-align: center; padding: 12px; background: #f7fafc; border-radius: 8px;
border: 1px solid #e2e8f0; }}
.source-count {{ font-size: 24px; font-weight: 700; color: #2b6cb0; }}
.footer {{ text-align: center; padding: 20px; font-size: 12px; color: #718096; }}
@media (max-width: 768px) {{
.kpi-grid {{ grid-template-columns: repeat(2, 1fr); }}
.source-grid {{ grid-template-columns: repeat(2, 1fr); }}
table.main-table {{ font-size: 11px; }}
}}
</style>
</head>
<body>
<div class="header">
<h1>🏕️ 스페이스락 입찰 인텔리전스 리포트</h1>
<p>수집일: {date_str} 오전 10:00 KST | 자동 수집: 나라장터(G2B) + S2B + 벤처나라 + D2B + LPS</p>
</div>
<div class="container">
<!-- KPI -->
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-value">{len(bids)}</div>
<div class="kpi-label">📋 총 수집 공고</div>
</div>
<div class="kpi-card" style="border-color:#fc8181">
<div class="kpi-value" style="color:#c53030">{len(grade_a)}</div>
<div class="kpi-label">🔴 A등급 즉시대응</div>
</div>
<div class="kpi-card" style="border-color:#68d391">
<div class="kpi-value" style="color:#276749">{total_amt:,}</div>
<div class="kpi-label">💰 총 추정 사업금액(원)</div>
</div>
<div class="kpi-card" style="border-color:#f6e05e">
<div class="kpi-value" style="color:#b7791f">{len(grade_b)}</div>
<div class="kpi-label">🟡 B등급 검토 필요</div>
</div>
</div>
<!-- 5대 필수항목 메인 테이블 -->
<div class="section">
<div class="section-title">📊 전체 입찰 공고 목록 (5대 필수항목)</div>
<div style="overflow-x:auto">
<table class="main-table">
<thead>
<tr>
<th>등급</th>
<th>사업명 ★</th>
<th>입찰공고품목 ★</th>
<th>사업금액 ★</th>
<th>제출시기(마감) ★</th>
<th>입찰시기(마감) ★</th>
<th>개찰일</th>
<th>발주처</th>
<th>출처</th>
<th>연관점수</th>
</tr>
</thead>
<tbody>
{rows_html}
</tbody>
</table>
</div>
<p style="font-size:11px;color:#718096;margin-top:8px">★ 표시: 5대 필수 항목</p>
</div>
<!-- 상세 카드 -->
<div class="section">
<div class="section-title">🔍 A·B등급 상세 분석</div>
{cards_html if cards_html else '<p style="color:#718096">해당 등급 공고가 없습니다.</p>'}
</div>
<!-- 차트 -->
<div class="section">
<div class="section-title">📈 분석 차트</div>
<canvas id="gradeChart" width="400" height="200"></canvas>
</div>
</div>
<div class="footer">
수집 시각: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} KST |
나라장터 G2B + S2B + 벤처나라 + D2B + LPS 통합 수집<br>
스페이스락 입찰 인텔리전스 by 다풀림행정사 사무소 | 010-3404-5955 (백상진 행정사) | [email protected]
</div>
<script>
const ctx = document.getElementById('gradeChart').getContext('2d');
new Chart(ctx, {{
type: 'doughnut',
data: {{
labels: ['🔴 A등급', '🟡 B등급', '🟢 C등급'],
datasets: [{{
data: [{len(grade_a)}, {len(grade_b)}, {len(grade_c)}],
backgroundColor: ['#fc8181','#f6e05e','#68d391'],
}}]
}},
options: {{ responsive: true, plugins: {{ legend: {{ position: 'bottom' }} }} }}
}});
</script>
</body>
</html>"""
return html
# scripts/main_runner.py
"""
스페이스락 입찰 인텔리전스 — 일일 자동 실행 메인 스크립트
실행 시각: 매일 오전 10:00 KST (Zapier 트리거)
"""
import sys, os, json, base64, requests
from datetime import datetime
# 스크립트 경로 추가
sys.path.insert(0, os.path.dirname(__file__))
from g2b_collector import collect_all_g2b, extract_five_fields, get_detail
from s2b_crawler import search_s2b
from venture_crawler import search_venturena
from lps_crawler import search_lps
from d2b_crawler import search_d2b
from html_generator import generate_html_report
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") # 환경변수로 주입
GITHUB_REPO = "sjbaik0431/claude"
KEYWORDS_SHORT = ["텐트", "천막", "오버레이", "임시구조물"]
def score_bid(bid):
"""연관성 점수 산정"""
score = 0
name = (bid.get("사업명","") + bid.get("입찰공고품목","")).lower()
# 키워드 매칭
tier1 = ["오버레이","텐트","천막","임시구조물","overlay","에어돔"]
tier2 = ["행사","이벤트","전시","박람회","야외공연","축제"]
tier3 = ["야외","임시","설치","렌탈","임대"]
if any(k in name for k in tier1): score += 30
elif any(k in name for k in tier2): score += 20
elif any(k in name for k in tier3): score += 10
# 금액
amt_str = str(bid.get("사업금액","0")).replace(",","").replace("원","").strip()
try:
amt = int(amt_str)
if amt >= 100_000_000: score += 30
elif amt >= 50_000_000: score += 20
elif amt >= 10_000_000: score += 10
else: score += 5
except: score += 5
# 시기
now_month = datetime.now().month
if now_month in [3,4,5,9,10,11]: score += 20
else: score += 10
# 방식
method = bid.get("입찰방식","")
if "일반" in method: score += 20
elif "제한" in method: score += 15
elif "지명" in method: score += 5
else: score += 15
return min(score, 100)
def run_daily():
today = datetime.now().strftime("%Y%m%d")
print(f"\n{'='*60}")
print(f"스페이스락 입찰 수집 시작: {today} 10:00 KST")
print(f"{'='*60}")
all_bids = []
# 1. G2B API
print("\n[1/5] 나라장터 G2B API 수집 중...")
g2b_bids = collect_all_g2b()
for b in g2b_bids:
bid_no = b.get("bidNtceNo","")
detail = get_detail(bid_no, b.get("_type","용역")) if bid_no else {}
five = extract_five_fields(b, detail)
five["source"] = "나라장터(G2B)"
five["발주처"] = b.get("dminsttNm","")
five["입찰방식"] = b.get("bidMthdNm","")
five["_raw"] = b
all_bids.append(five)
print(f" → G2B {len(g2b_bids)}건 수집")
# 2. S2B
print("\n[2/5] 학교장터(S2B) 크롤링 중...")
for kw in KEYWORDS_SHORT:
all_bids.extend(search_s2b(kw))
print(f" → S2B 수집 완료")
# 3. 벤처나라
print("\n[3/5] 벤처나라 크롤링 중...")
for kw in KEYWORDS_SHORT:
all_bids.extend(search_venturena(kw))
print(f" → 벤처나라 수집 완료")
# 4. LPS
print("\n[4/5] 지방계약정보(LPS) 크롤링 중...")
for kw in KEYWORDS_SHORT:
all_bids.extend(search_lps(kw))
print(f" → LPS 수집 완료")
# 5. D2B
print("\n[5/5] 국방전자조달(D2B) 크롤링 중...")
for kw in KEYWORDS_SHORT:
all_bids.extend(search_d2b(kw))
print(f" → D2B 수집 완료")
# 중복 제거
seen = set()
unique = []
for b in all_bids:
key = b.get("사업명","") + b.get("발주처","")
if key and key not in seen:
seen.add(key)
unique.append(b)
# 점수화
for b in unique:
b["score"] = score_bid(b)
print(f"\n✅ 총 {len(unique)}건 수집 완료")
# HTML 생성
date_str = datetime.now().strftime("%Y년 %m월 %d일")
html = generate_html_report(unique, date_str)
filename = f"rent_spacerock_bid_{today}.html"
# GitHub 업로드
upload_to_github(html, filename)
print(f"\n🌐 보고서: https://sjbaik0431.github.io/claude/{filename}")
return filename, unique
def upload_to_github(content, filename):
"""GitHub Pages 업로드"""
import base64 as b64
url = f"https://api.github.com/repos/{GITHUB_REPO}/contents/{filename}"
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Content-Type": "application/json"
}
# SHA 조회
sha = None
r = requests.get(url, headers=headers)
if r.status_code == 200:
sha = r.json().get("sha")
body = {
"message": f"[RENT] {filename} 자동 업데이트",
"content": b64.b64encode(content.encode("utf-8")).decode("utf-8"),
}
if sha:
body["sha"] = sha
r2 = requests.put(url, json=body, headers=headers)
if r2.status_code in [200, 201]:
print(f"✅ GitHub 업로드 성공: {filename}")
else:
print(f"❌ GitHub 업로드 실패: {r2.status_code} {r2.text[:200]}")
if __name__ == "__main__":
run_daily()
RECIPIENTS = {
"대표님": "대표이메일@spacerock.kr", # Q가 지정
"Q (본인)": "q이메일@spacerock.kr",
}
# Gmail MCP 또는 Zapier Gmail 사용
# 발송 제목: [스페이스락] 입찰 인텔리전스 — YYYY년 MM월 DD일
# 본문: KPI 요약 + A등급 목록 + 보고서 링크
⚠️ 이메일 주소는 Q가 직접 대화에서 제공해야 한다. Claude는 추정하지 않는다.
이름: 스페이스락 일일 입찰 수집 (매일 10:00 KST)
Trigger: Schedule by Zapier
- Frequency: Every Day
- Time of Day: 10:00 AM
- Timezone: Asia/Seoul
Action 1: Code by Zapier (또는 Webhook)
- URL: [Claude API 호출 또는 Python 서버 엔드포인트]
- Method: POST
- 명령: "RENT 스킬 실행 — 오늘 입찰 공고 수집 및 보고서 생성"
Action 2: Gmail
- To: Q 지정 이메일
- Subject: [스페이스락] 입찰 인텔리전스 — {오늘날짜}
- Body: 수집 결과 요약 + GitHub Pages 링크
# .github/workflows/rent-daily.yml