렌탈

스페이스락(SpaceRock) 오버레이·대형텐트 렌탈 전문기업의 입찰 인텔리전스 자동화 스킬. 나라장터 및 민간 채널에서 천막·오버레이·임시시설물 관련 입찰공고를 자동 수집·분석하고 HTML 보고서를 생성해 GitHub에 업로드하고 지정 이메일로 발송한다. 반드시 이 스킬을 사용해야 하는 상황: - "렌탈 RENT" 입력 시 (핵심 트리거 — 반드시 즉시 실행) - "렌탈 스킬", "입찰 찾아줘", "나라장터 검색", "공고 분석" 언급 시 - "오버레이", "대형텐트", "천막 렌탈", "임시시설물" + 입찰/공고 맥락 - "이번 주 공고", "신규 입찰 있어?", "스페이스락 입찰" 언급 시 - "보고서 보내줘", "대표님께 공유", "이메일 발송" + 입찰 맥락 - Q가 영업 준비, 마케팅 대응, 투두 리스트 작성을 요청하는 모든 상황

렌탈 스킬 — SpaceRock 입찰 인텔리전스 자동 실행 시스템

회사 고정 정보 (항상 참조)

회사명  : ㈜스페이스락® (SpaceRock)
홈페이지: www.spacerock.kr
핵심제품: 오버레이(Overlay), 대형텐트, MQ텐트, 몽골텐트, 임시관람석, 가설구조물
주요실적: 2018 평창동계올림픽, 아시안게임, 충주세계조정선수권, 국제무예마스터쉽, 국방산업대전
담당자  : 마케팅 영업 Q
GitHub  : sjbaik0431/claude
토큰    : userPreferences의 GITHUB_TOKEN 사용

실행 전 필수 확인

스킬 시작 시 Q에게 아래를 간단히 확인한다 (이미 대화에서 언급됐으면 생략):

  1. 검색 기간: 오늘 기준 최근 몇 주? (기본값: 4주)
  2. 이메일 수신자: 보고서를 보낼 메일 주소 (미지정이면 생성 후 URL만 전달)
  3. G2B API 키: 보유 여부 (없으면 web_search 폴백 자동 적용)

확인 즉시 아래 PHASE를 순서대로 완전 자동 실행한다.


PHASE 1 — 입찰 공고 수집

키워드 목록 (순서대로 전부 검색)

KEYWORDS_1 = [
    "오버레이", "overlay", "대형텐트", "MQ텐트", "몽골텐트",
    "임시시설물", "임시관람석", "천막 임대", "가설구조물",
    "야외행사시설", "캐노피 설치", "행사용 텐트"
]
KEYWORDS_2 = [
    "행사 렌탈", "이벤트 시설", "박람회 시설",
    "축제 시설", "관람석 설치", "무대 구조물", "임시 부스"
]

1-A: G2B API 수집 (API 키 보유 시)

import urllib.request, json, urllib.parse

G2B_URL = "https://apis.data.go.kr/1230000/BidPublicInfoService04/getBidPblancListInfoServc"

def fetch_g2b(keyword, service_key, days=28):
    from datetime import datetime, timedelta
    today = datetime.now()
    start = (today - timedelta(days=days)).strftime("%Y%m%d") + "0000"
    end = today.strftime("%Y%m%d") + "2359"
    params = urllib.parse.urlencode({
        "serviceKey": service_key,
        "pageNo": 1, "numOfRows": 100,
        "type": "json", "inqryDiv": 1,
        "bidNtceNm": keyword,
        "inqryBgnDt": start, "inqryEndDt": end
    })
    req = urllib.request.Request(f"{G2B_URL}?{params}")
    try:
        res = json.loads(urllib.request.urlopen(req, timeout=10).read())
        items = res.get("response",{}).get("body",{}).get("items",[])
        return items if isinstance(items, list) else [items] if items else []
    except:
        return []

각 키워드로 fetch_g2b() 호출 → 결과를 raw_bids 리스트에 합산 (중복 제거: bidNtceNo 기준)

1-B: web_search 폴백 (API 키 없거나 결과 부족 시)

web_search 도구로 아래 쿼리를 모두 실행한다:

나라장터 입찰공고 천막 오버레이 임시시설물 2026
나라장터 입찰공고 대형텐트 행사시설 렌탈 2026
나라장터 "임시관람석" OR "가설구조물" OR "MQ텐트" 입찰
나라장터 "박람회" OR "축제" OR "스포츠대회" 천막 설치 용역 2026
site:g2b.go.kr 오버레이 텐트 임시시설
스페이스락 경쟁 오버레이 렌탈 입찰 2026

검색 결과에서 아래 정보를 추출해 raw_bids에 추가:

  • 공고명, 발주처, 공고일, 마감일, 추정금액, URL

1-C: 수집 결과 정규화

수집된 모든 항목을 아래 스키마로 정규화:

BID = {
    "id"          : "공고번호 또는 임시 ID",
    "title"       : "공고명",
    "issuer"      : "발주처명",
    "issuer_type" : "중앙부처|지자체|공공기관|민간",
    "region"      : "지역",
    "announce_dt" : "YYYY-MM-DD",
    "deadline"    : "YYYY-MM-DD",
    "amount_won"  : 숫자 (원, 없으면 0),
    "amount_str"  : "X억" 또는 "미상",
    "contract"    : "일반경쟁|제한경쟁|수의계약|미상",
    "keywords"    : ["매칭된 키워드 목록"],
    "score"       : 0,        # PHASE 2에서 채움
    "priority"    : "HIGH",   # PHASE 2에서 채움
    "reason"      : "",       # PHASE 2에서 채움
    "url"         : "공고 URL"
}

PHASE 2 — 연관성 자동 채점 및 우선순위 분류

각 공고에 대해 아래 5개 기준으로 점수 합산:

def score_bid(bid):
    score = 0

    # ① 키워드 매칭 (최대 40점)
    kw1 = ["오버레이","대형텐트","임시시설물","임시관람석","MQ텐트","몽골텐트","가설구조물","천막임대"]
    kw2 = ["행사렌탈","박람회시설","관람석","무대구조물","임시부스","캐노피"]
    title_low = bid["title"].replace(" ","")
    hit1 = sum(1 for k in kw1 if k.replace(" ","") in title_low)
    hit2 = sum(1 for k in kw2 if k.replace(" ","") in title_low)
    score += min(hit1 * 12 + hit2 * 6, 40)

    # ② 사업 규모 (최대 20점)
    amt = bid["amount_won"]
    if   amt >= 1_000_000_000: score += 20
    elif amt >= 500_000_000:   score += 15
    elif amt >= 100_000_000:   score += 10
    elif amt > 0:              score +=  5
    else:                      score +=  3  # 미상

    # ③ 발주처 성격 (최대 15점)
    itype = bid["issuer_type"]
    if   itype == "중앙부처":  score += 15
    elif itype == "공공기관":  score += 12
    elif itype == "지자체":    score += 10
    else:                      score +=  7

    # ④ 시기 적합성 (최대 15점)
    from datetime import datetime
    try:
        dl = datetime.strptime(bid["deadline"], "%Y-%m-%d")
        days_left = (dl - datetime.now()).days
        if   days_left <= 30:  score += 15
        elif days_left <= 60:  score += 12
        elif days_left <= 90:  score += 8
        else:                  score +=  5
    except: score += 5

    # ⑤ 지역 접근성 (최대 10점)
    region = bid["region"]
    if   "서울" in region or "경기" in region: score += 9
    elif "인천" in region or "충청" in region: score += 10
    elif "전국" in region:                      score += 10
    else:                                        score += 7

    # 우선순위 결정
    if   score >= 80: priority = "HIGH"
    elif score >= 60: priority = "MEDIUM"
    elif score >= 40: priority = "LOW"
    else:             priority = "SKIP"

    return min(score, 100), priority

for bid in raw_bids:
    bid["score"], bid["priority"] = score_bid(bid)

# SKIP 제외 후 점수 내림차순 정렬
bids = sorted([b for b in raw_bids if b["priority"] != "SKIP"],
              key=lambda x: x["score"], reverse=True)

PHASE 3 — HTML 보고서 생성

아래 Python 스크립트를 /home/claude/generate_rental_report.py로 저장 후 실행:

# generate_rental_report.py
import json
from datetime import datetime

TODAY = datetime.now().strftime("%Y.%m.%d")
TODAY_FILE = datetime.now().strftime("%Y%m%d")
FILENAME = f"rental_bid_report_{TODAY_FILE}.html"

def build_html(bids):
    total = len(bids)
    high = sum(1 for b in bids if b["priority"]=="HIGH")
    medium = sum(1 for b in bids if b["priority"]=="MEDIUM")
    total_amt = sum(b["amount_won"] for b in bids)
    avg_score = round(sum(b["score"] for b in bids)/total,1) if total else 0
    amt_str = f"{total_amt/100_000_000:.1f}억" if total_amt>0 else "집계중"

    def badge(p):
        color = {"HIGH":"#ef4444","MEDIUM":"#f59e0b","LOW":"#22c55e"}.get(p,"#94a3b8")
        icon  = {"HIGH":"🔴","MEDIUM":"🟡","LOW":"🟢"}.get(p,"⚪")
        return f'<span style="background:{color}20;color:{color};padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;white-space:nowrap">{icon} {p}</span>'

    def row(b):
        bar_color = "#ef4444" if b["score"]>=80 else "#f59e0b" if b["score"]>=60 else "#22c55e"
        return f"""
        <tr>
          <td style="padding:12px 14px">{badge(b["priority"])}</td>
          <td style="padding:12px 14px">
            <div style="font-weight:600;font-size:13px;margin-bottom:4px">{b["title"]}</div>
            <div style="display:flex;flex-wrap:wrap;gap:3px">
              {"".join(f'<span style="background:#f1f5f9;color:#475569;font-size:10px;padding:1px 6px;border-radius:3px">{k}</span>' for k in b.get("keywords",[])[:4])}
            </div>
          </td>
          <td style="padding:12px 14px;font-size:12px">{b["issuer"]}<br>
            <span style="background:#eff6ff;color:#1565c0;font-size:10px;padding:1px 7px;border-radius:3px;font-weight:600">{b["issuer_type"]}</span>
          </td>
          <td style="padding:12px 14px;font-size:12px">{b["region"]}</td>
          <td style="padding:12px 14px;font-size:12px;font-weight:700;color:{"#ef4444" if b["priority"]=="HIGH" else "#b45309" if b["priority"]=="MEDIUM" else "#475569"}">{b["deadline"]}</td>
          <td style="padding:12px 14px;font-weight:700;color:#0a3d8f">{b["amount_str"]}</td>
          <td style="padding:12px 14px">
            <div style="display:flex;align-items:center;gap:6px">
              <div style="flex:1;height:6px;background:#e2e8f0;border-radius:3px;overflow:hidden">
                <div style="width:{b["score"]}%;height:100%;background:{bar_color};border-radius:3px"></div>
              </div>
              <span style="font-weight:700;font-size:12px;min-width:26px">{b["score"]}</span>
            </div>
          </td>
        </tr>"""

    def todo_card(b):
        if b["priority"] not in ("HIGH","MEDIUM"): return ""
        border = "#ef4444" if b["priority"]=="HIGH" else "#f59e0b"
        items = [
            ("공고 원문 다운로드 및 참가 자격 확인","D-0"),
            ("발주처 담당자 연락처 확인 및 사전 컨택","D+1"),
            ("유사 레퍼런스 선별 (평창·아시안게임 등)","D+2"),
            ("견적 초안 작성 및 장비 가용성 확인","D+3"),
            ("제안서 초안 완성 + 내부 가격 협의","D+5"),
            ("전자입찰서 제출 (나라장터 로그인)","D-3"),
        ]
        rows = "".join(f"""
          <div onclick="this.classList.toggle('done')" style="display:flex;align-items:flex-start;gap:10px;padding:8px 0;border-bottom:1px solid #f8faff;cursor:pointer">
            <div style="width:18px;height:18px;border:2px solid #cbd5e1;border-radius:4px;flex-shrink:0;margin-top:1px"></div>
            <div style="font-size:12px;line-height:1.5"><span style="color:#94a3b8;font-size:10px;font-weight:600">[{tag}]</span> {txt}</div>
          </div>""" for txt,tag in items)
        return f"""
        <div style="background:#fff;border-radius:14px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,0.06);border-top:4px solid {border}">
          <div style="font-size:13px;font-weight:800;margin-bottom:4px">{b["title"][:40]}{"..." if len(b["title"])>40 else ""}</div>
          <div style="font-size:11px;color:#64748b;margin-bottom:12px">{b["issuer"]} | 마감: {b["deadline"]} | {b["amount_str"]}</div>
          {rows}
        </div>"""

    bid_rows = "\n".join(row(b) for b in bids)
    todo_cards = "\n".join(todo_card(b) for b in bids[:6])

    # Chart.js data
    score_dist = [sum(1 for b in bids if 40<=b["score"]<60),
                  sum(1 for b in bids if 60<=b["score"]<80),
                  sum(1 for b in bids if 80<=b["score"]<90),
                  sum(1 for b in bids if b["score"]>=90)]
    issuer_cnt = {"중앙부처":0,"지자체":0,"공공기관":0,"민간":0}
    for b in bids: issuer_cnt[b["issuer_type"]] = issuer_cnt.get(b["issuer_type"],0)+1
    amt_dist = [sum(1 for b in bids if b["amount_won"]>=1e9),
                sum(1 for b in bids if 5e8<=b["amount_won"]<1e9),
                sum(1 for b in bids if 1e8<=b["amount_won"]<5e8),
                sum(1 for b in bids if 0<b["amount_won"]<1e8)]

    return f"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>스페이스락® 입찰 인텔리전스 {TODAY}</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{font-family:'Noto Sans KR',sans-serif;background:#f4f7fc;color:#1a1a2e}}
.hd{{background:linear-gradient(135deg,#0a3d8f,#1565c0);color:#fff;padding:24px 36px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}}
.hd-l{{display:flex;align-items:center;gap:14px}}
.logo{{width:52px;height:52px;border-radius:10px;background:rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:900}}
.ct{{max-width:1280px;margin:0 auto;padding:28px 20px}}
.kpi-g{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:24px}}
.kpi{{background:#fff;border-radius:14px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,.06);border-left:4px solid #0a3d8f;position:relative;overflow:hidden}}
.kpi.r{{border-left-color:#ef4444}}.kpi.y{{border-left-color:#f59e0b}}.kpi.p{{border-left-color:#8b5cf6}}
.kv{{font-size:32px;font-weight:900;color:#0a3d8f;margin:6px 0 3px}}
.kl{{font-size:11px;color:#64748b;font-weight:600;text-transform:uppercase}}
.ks{{font-size:11px;color:#94a3b8}}
.ic{{position:absolute;top:16px;right:16px;font-size:28px;opacity:.12}}
.cg{{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:24px}}
.cc{{background:#fff;border-radius:14px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,.06)}}
.ct2{{font-size:13px;font-weight:700;margin-bottom:14px}}
.cw{{position:relative;height:190px}}
.sec{{margin-bottom:28px}}
.sh{{display:flex;align-items:center;gap:10px;margin-bottom:14px}}
.st{{font-size:17px;font-weight:800;color:#0a3d8f}}
.sb{{background:#0a3d8f;color:#fff;font-size:11px;font-weight:700;padding:2px 10px;border-radius:20px}}
.tw{{background:#fff;border-radius:14px;box-shadow:0 2px 10px rgba(0,0,0,.06);overflow:hidden}}
.tb{{padding:14px 18px;border-bottom:1px solid #e2e8f0;display:flex;gap:10px;flex-wrap:wrap;align-items:center}}
.fb{{padding:6px 14px;border-radius:20px;border:2px solid #e2e8f0;background:#fff;cursor:pointer;font-size:12px;font-weight:600}}
.fb.a{{background:#0a3d8f;color:#fff;border-color:#0a3d8f}}
.sb2{{padding:6px 14px;border-radius:20px;border:2px solid #e2e8f0;font-size:13px;outline:none;margin-left:auto}}
table{{width:100%;border-collapse:collapse}}
thead{{background:#f8faff}}
th{{padding:11px 14px;text-align:left;font-size:10px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px}}
td{{border-top:1px solid #e2e8f0;vertical-align:middle}}
tr:hover td{{background:#f8faff}}
.tg{{display:grid;grid-template-columns:1fr 1fr;gap:16px}}
.ft{{background:#0a3d8f;color:rgba(255,255,255,.75);padding:20px 36px;margin-top:32px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}}
.done > div:last-child{{text-decoration:line-through;color:#94a3b8}}
.done > div:first-child{{background:#0a3d8f!important;border-color:#0a3d8f!important}}
.live{{display:inline-block;background:#ff6b35;color:#fff;font-size:11px;font-weight:700;padding:2px 10px;border-radius:20px;animation:pl 2s infinite}}
@keyframes pl{{0%,100%{{opacity:1}}50%{{opacity:.5}}}}
@media(max-width:900px){{.kpi-g,.cg,.tg{{grid-template-columns:1fr 1fr}}}}</style>
</head>
<body>
<div class="hd">
  <div class="hd-l">
    <div class="logo">SR</div>
    <div>
      <div style="font-size:18px;font-weight:800">㈜스페이스락®</div>
      <div style="font-size:12px;opacity:.75">SpaceRock · 오버레이&대형텐트 전문기업</div>
    </div>
  </div>
  <div style="text-align:right">
    <div style="font-size:12px;opacity:.7">📅 {TODAY} 생성</div>
    <div style="font-size:17px;font-weight:700;margin-top:3px">🎯 입찰 인텔리전스 보고서</div>
    <span class="live">● LIVE</span>
  </div>
</div>
<div class="ct">
  <!-- KPI -->
  <div class="kpi-g">
    <div class="kpi"><div class="ic">📋</div><div class="kl">수집 공고</div><div class="kv">{total}</div><div class="ks">전체 분석 완료</div></div>
    <div class="kpi r"><div class="ic">🔴</div><div class="kl">즉시 대응</div><div class="kv">{high}</div><div class="ks">HIGH 우선순위</div></div>
    <div class="kpi y"><div class="ic">🟡</div><div class="kl">검토 대상</div><div class="kv">{medium}</div><div class="ks">MEDIUM 우선순위</div></div>
    <div class="kpi p"><div class="ic">💰</div><div class="kl">총 추정 금액</div><div class="kv" style="font-size:24px">{amt_str}</div><div class="ks">평균 연관도 {avg_score}점</div></div>
  </div>
  <!-- CHARTS -->
  <div class="cg">
    <div class="cc"><div class="ct2">📊 연관성 점수 분포</div><div class="cw"><canvas id="c1"></canvas></div></div>
    <div class="cc"><div class="ct2">🏢 발주처 유형별</div><div class="cw"><canvas id="c2"></canvas></div></div>
    <div class="cc"><div class="ct2">💵 사업 규모 분포</div><div class="cw"><canvas id="c3"></canvas></div></div>
  </div>
  <!-- BID TABLE -->
  <div class="sec">
    <div class="sh"><div class="st">📋 입찰 공고 목록</div><span class="sb">{total}건</span></div>
    <div class="tw">
      <div class="tb">
        <button class="fb a" onclick="ft('ALL',this)">전체</button>
        <button class="fb" onclick="ft('HIGH',this)">🔴 HIGH ({high})</button>
        <button class="fb" onclick="ft('MEDIUM',this)">🟡 MEDIUM ({medium})</button>
        <button class="fb" onclick="ft('LOW',this)">🟢 LOW</button>
        <input class="sb2" type="text" placeholder="🔍 검색..." oninput="fs(this.value)">
      </div>
      <div style="overflow-x:auto">
        <table><thead><tr>
          <th>우선순위</th><th>공고명</th><th>발주처</th><th>지역</th>
          <th>마감일</th><th>추정금액</th><th>연관도</th>
        </tr></thead>
        <tbody id="tb">{bid_rows}</tbody></table>
      </div>
    </div>
  </div>
  <!-- TODO -->
  <div class="sec">
    <div class="sh"><div class="st">✅ To-Do 리스트</div></div>
    <div class="tg">{todo_cards}</div>
  </div>
  <!-- MARKETING PLAN -->
  <div class="sec">
    <div class="sh"><div class="st">🚀 마케팅 대응 전략</div></div>
    <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px">
      <div style="background:#fff;border-radius:14px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,.06)">
        <div style="font-size:13px;font-weight:800;color:#0a3d8f;margin-bottom:12px">📨 발주처별 맞춤 접근</div>
        <div style="font-size:12px;line-height:1.8;color:#475569">• 지자체 → 기술 안전성 강조 (구조계산서·KC인증)<br>• 스포츠 대회 → 평창올림픽·아시안게임 레퍼런스<br>• 박람회/민간 → 디자인 커스터마이징 강조<br>• 국방/공공기관 → 납기 준수·24시간 AS 체계</div>
      </div>
      <div style="background:#fff;border-radius:14px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,.06)">
        <div style="font-size:13px;font-weight:800;color:#0a3d8f;margin-bottom:12px">📑 제안서 차별화 포인트</div>
        <div style="font-size:12px;line-height:1.8;color:#475569">• 20년 실적 + 국제대회 수행 경험<br>• 원스탑 솔루션 (설계→설치→철거→원상복구)<br>• 친환경 오버레이 (재사용·폐기물 제로) 소구<br>• 현장 답사 후 1주일 내 견적 제시 약속</div>
      </div>
      <div style="background:#fff;border-radius:14px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,.06)">
        <div style="font-size:13px;font-weight:800;color:#0a3d8f;margin-bottom:12px">🔑 나라장터 API 키 발급</div>
        <div style="font-size:12px;line-height:1.8;color:#475569">① data.go.kr 접속 후 회원가입<br>② "입찰공고정보서비스" 검색<br>③ 활용신청 (즉시~1일 승인)<br>④ 발급 키 → Q에게 전달 → 자동수집 시작!</div>
      </div>
    </div>
  </div>
</div>
<div class="ft">
  <div>
    <div style="color:#fff;font-size:15px;font-weight:800">㈜스페이스락® | www.spacerock.kr</div>
    <div style="margin-top:3px;font-size:12px">담당: 마케팅 영업 Q | 본 보고서는 렌탈 스킬에 의해 자동 생성됨</div>
  </div>
  <div style="text-align:right;font-size:12px">
    연락처: 010-3404-5955 / [email protected]
  </div>
</div>
<script>
const ALL_ROWS=[...document.querySelectorAll("#tb tr")];
function ft(p,btn){{
  document.querySelectorAll(".fb").forEach(b=>b.classList.remove("a"));
  btn.classList.add("a");
  ALL_ROWS.forEach(r=>r.style.display=p==="ALL"||r.innerText.includes(p)?"":"none");
}}
function fs(q){{ALL_ROWS.forEach(r=>r.style.display=r.innerText.toLowerCase().includes(q.toLowerCase())?"":"none");}}
const opt={{responsive:true,maintainAspectRatio:false,plugins:{{legend:{{position:"bottom",labels:{{font:{{size:11}},boxWidth:12}}}}}}}};
new Chart(document.getElementById("c1"),{{type:"bar",data:{{labels:["40~59 LOW","60~79 MED","80~89 HIGH","90+ TOP"],datasets:[{{label:"공고수",data:{score_dist},backgroundColor:["#86efac","#fcd34d","#f87171","#dc2626"],borderRadius:6}}]}},options:{{...opt,plugins:{{legend:{{display:false}}}},scales:{{y:{{beginAtZero:true}}}}}}}});
new Chart(document.getElementById("c2"),{{type:"doughnut",data:{{labels:{list(issuer_cnt.keys())},datasets:[{{data:{list(issuer_cnt.values())},backgroundColor:["#3b82f6","#8b5cf6","#ef4444","#f59e0b"],borderWidth:2,borderColor:"#fff"}}]}},options:opt}});
new Chart(document.getElementById("c3"),{{type:"pie",data:{{labels:["10억+","5~10억","1~5억","1억미만"],datasets:[{{data:{amt_dist},backgroundColor:["#1d4ed8","#3b82f6","#93c5fd","#dbeafe"],borderWidth:2,borderColor:"#fff"}}]}},options:opt}});
</script>
</body></html>"""

if __name__ == "__main__":
    import sys
    bids_json = sys.argv[1] if len(sys.argv)>1 else "[]"
    bids = json.loads(bids_json)
    if not bids:   # 샘플 데이터 (API/검색 결과 없을 때 데모)
        bids = [
            {"id":"DEMO-001","title":"2026 전국체육대회 임시시설물 설치공사","issuer":"경기도체육회","issuer_type":"공공기관","region":"경기도","announce_dt":"2026-04-01","deadline":"2026-04-20","amount_won":1800000000,"amount_str":"18억","contract":"일반경쟁","keywords":["임시시설물","대형텐트","관람석"],"score":92,"priority":"HIGH","reason":"임시시설물 대규모 공공기관","url":"#"},
            {"id":"DEMO-002","title":"2026 세계수영선수권 오버레이 시설 렌탈","issuer":"문화체육관광부","issuer_type":"중앙부처","region":"광주","announce_dt":"2026-03-28","deadline":"2026-04-25","amount_won":2500000000,"amount_str":"25억","contract":"제한경쟁","keywords":["오버레이","임시관람석","MQ텐트"],"score":97,"priority":"HIGH","reason":"오버레이 직접 명시 국제대회","url":"#"},
            {"id":"DEMO-003","title":"대전 국제박람회 야외 전시 천막 설치용역","issuer":"대전광역시","issuer_type":"지자체","region":"대전","announce_dt":"2026-04-02","deadline":"2026-05-15","amount_won":1200000000,"amount_str":"12억","contract":"일반경쟁","keywords":["천막","대형텐트","박람회시설"],"score":88,"priority":"HIGH","reason":"천막 설치 대형 박람회","url":"#"},
            {"id":"DEMO-004","title":"2026 국방산업대전 임시시설물 설치","issuer":"방위사업청","issuer_type":"중앙부처","region":"경남 창원","announce_dt":"2026-04-01","deadline":"2026-05-10","amount_won":900000000,"amount_str":"9억","contract":"제한경쟁","keywords":["임시시설물","가설구조물","행사렌탈"],"score":85,"priority":"HIGH","reason":"임시시설물 중앙부처 대규모","url":"#"},
            {"id":"DEMO-005","title":"부산 국제모터쇼 임시 천막 및 구조물 렌탈","issuer":"부산시설공단","issuer_type":"공공기관","region":"부산","announce_dt":"2026-04-03","deadline":"2026-05-30","amount_won":800000000,"amount_str":"8억","contract":"일반경쟁","keywords":["천막","임시구조물","행사렌탈"],"score":82,"priority":"HIGH","reason":"천막 렌탈 대형 국제행사","url":"#"},
            {"id":"DEMO-006","title":"광주 김치세계화 박람회 야외 부스 설치","issuer":"광주광역시","issuer_type":"지자체","region":"광주","announce_dt":"2026-04-04","deadline":"2026-06-15","amount_won":400000000,"amount_str":"4억","contract":"일반경쟁","keywords":["박람회시설","전시천막","야외전시"],"score":74,"priority":"MEDIUM","reason":"박람회 야외시설 지자체","url":"#"},
            {"id":"DEMO-007","title":"2026 충주세계무술선수권 임시구조물","issuer":"충주시","issuer_type":"지자체","region":"충북 충주","announce_dt":"2026-04-05","deadline":"2026-07-01","amount_won":600000000,"amount_str":"6억","contract":"일반경쟁","keywords":["임시구조물","관람석","스포츠"],"score":77,"priority":"MEDIUM","reason":"임시구조물 국제스포츠","url":"#"},
            {"id":"DEMO-008","title":"서울 한강 페스티벌 야외 천막 렌탈","issuer":"서울시설공단","issuer_type":"공공기관","region":"서울","announce_dt":"2026-04-05","deadline":"2026-07-10","amount_won":500000000,"amount_str":"5억","contract":"일반경쟁","keywords":["천막","야외행사","행사렌탈"],"score":68,"priority":"MEDIUM","reason":"천막 렌탈 서울 대형","url":"#"},
            {"id":"DEMO-009","title":"속초 설악문화제 행사 부스 렌탈","issuer":"속초시","issuer_type":"지자체","region":"강원 속초","announce_dt":"2026-04-06","deadline":"2026-08-15","amount_won":80000000,"amount_str":"8천만","contract":"수의계약","keywords":["행사렌탈","천막"],"score":52,"priority":"LOW","reason":"소규모 지역 행사","url":"#"},
        ]
    html = build_html(bids)
    out = f"/home/claude/{FILENAME}"
    with open(out,"w",encoding="utf-8") as f:
        f.write(html)
    print(f"SAVED:{out}:{FILENAME}")

스크립트 실행:

python3 /home/claude/generate_rental_report.py '[]' 

실제 공고 데이터가 있으면 JSON 배열로 전달, 없으면 '[]'로 실행 (샘플 데이터 사용)

출력에서 SAVED:/home/claude/{파일명}:{파일명} 형태로 경로 확인 후 PHASE 4로 이동.


PHASE 4 — GitHub 업로드

# upload_to_github.py
import urllib.request, base64, json, sys

def github_upload(filepath, filename, token, repo="sjbaik0431/claude"):
    api_url = f"https://api.github.com/repos/{repo}/contents/{filename}"
    headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}

    with open(filepath, "rb") as f:
        content_b64 = base64.b64encode(f.read()).decode()

    # SHA 조회 (기존 파일 있으면 필요)
    sha = ""
    req = urllib.request.Request(api_url, headers=headers)
    try:
        res = urllib.request.urlopen(req)
        sha = json.loads(res.read()).get("sha", "")
    except: pass

    payload = {"message": f"🏕️ 렌탈 입찰 보고서 {filename}", "content": content_b64}
    if sha: payload["sha"] = sha

    req2 = urllib.request.Request(api_url,
        data=json.dumps(payload).encode(),
        headers=headers, method="PUT")
    urllib.request.urlopen(req2)
    print(f"OK:https://sjbaik0431.github.io/claude/{filename}")

if __name__ == "__main__":
    github_upload(sys.argv[1], sys.argv[2], sys.argv[3])
python3 /home/claude/upload_to_github.py \
  /home/claude/{FILENAME} \
  {FILENAME} \
  {GITHUB_TOKEN}

출력 URL https://sjbaik0431.github.io/claude/{FILENAME} 을 기록한다.


PHASE 5 — 이메일 발송

Gmail MCP gmail_send_email 도구 사용. 발송 전 반드시 Q에게 수신자를 확인한다.

이메일 형식:

제목: [SpaceRock 입찰 인텔리전스] {TODAY} 주간 공고 분석 보고서

안녕하세요,

스페이스락 입찰 인텔리전스 시스템이 금주 관련 공고 {total}건을 분석했습니다.

━━━━━━━━━━━━━━━━━━━━━━━━━
📊 금일 분석 요약
• 수집 공고: {total}건
• 즉시 대응 필요(HIGH): {high}건
• 총 추정 사업 금액: {amt_str}
━━━━━━━━━━━━━━━━━━━━━━━━━

🔴 긴급 대응 필요 TOP 3
{HIGH 공고 상위 3건: 공고명 / 발주처 / 마감일 / 금액}

📎 상세 보고서 (클릭하여 열기):
{GitHub Pages URL}

담당: 마케팅 영업 Q
㈜스페이스락® | www.spacerock.kr

PHASE 6 — 실행 결과 최종 보고

Q에게 아래 형식으로 간결하게 결과를 보고한다:

✅ 렌탈 스킬 실행 완료 — {TODAY}

📋 분석 결과
• 수집: {total}건 | 🔴 HIGH: {high}건 | 🟡 MEDIUM: {medium}건
• 총 추정 금액: {amt_str}

🔴 즉시 대응 TOP 3
1. {공고명} — {발주처} / D-{남은일수}일
2. {공고명} — {발주처} / D-{남은일수}일
3. {공고명} — {발주처} / D-{남은일수}일

🌐 보고서 URL
{GitHub Pages 공개 URL}

📧 이메일 발송: {완료/대기(수신자 미지정)}

오류 대응표

상황자동 대응
G2B API 응답 오류web_search 폴백 즉시 전환
검색 결과 0건키워드 확장 후 재시도, 그래도 없으면 "이번 주 해당 공고 없음" 명시
GitHub 업로드 실패/mnt/user-data/outputs/에 저장 후 수동 업로드 안내
이메일 수신자 미지정URL만 제공, 발송 생략
추정금액 파싱 불가amount_won=0, amount_str="미상" 처리

정기 실행 권장

주기내용
매일 08:00렌탈 RENT 입력 → 신규 공고 자동 수집
매주 월요일주간 종합 보고서 생성 + 이메일 발송
매월 1일월간 시장 트렌드 분석

연락처: 010-3404-5955 (백상진 행정사) / [email protected]