์คํ์ด์ค๋ฝ(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