fix(startup-support): use K-Startup proxy surface

This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-06-05 13:03:28 +09:00
commit 346ce7f516
9 changed files with 612 additions and 1132 deletions

View file

@ -1,221 +1,91 @@
# startup-support
## 스킬 개요
`startup-support` helps agents answer Korean startup support-program questions by searching K-Startup announcement data through `k-skill-proxy`.
`startup-support` 스킬은 정부, 지자체, 공기업이 제공하는 스타트업, 중소기업, 개인 창업가를 위한 지원사업 정보를 공식 API를 통해 조회하는 기능을 제공합니다.
Use it for questions such as:
## 사용 시나리오
- "서울 청년 창업 지원사업 찾아줘"
- "모집 중인 사업화 지원 공고 알려줘"
- "이번 달 확인할 K-Startup 공고를 요약해줘"
- "스타트업 지원사업 알려줘"
- "중소기업 보조금 종류 정리해줘"
- "서울시 창업 지원금 프로그램"
- "청년 창업 지원금 요건"
- "MVP 지원사업 목록"
- "정부 지원사업 마감일"
- "스타트업 융자 프로그램"
Do not use it for application submission, legal eligibility decisions, payment automation, or grant amount calculation. The final source of truth is always the official announcement URL returned in each result.
## 기능 특징
## Data Flow
### 1. 다양한 데이터 소스
- **공공데이터포털 (data.go.kr)**: 중소벤처기업부 스타트업 지원사업 API
- **지자체별 공식 사이트**: 서울시, 경기도, 부산시, 광주시, 대구시 등
- **공기업 및 기금 관리기관**: 중소기업진흥공단, 기술보증기금 등
The helper calls:
### 2. 전수 검색
- 모든 소스를 병렬로 검색하여 누락되는 지원사업이 없도록 함
- 실시간 정보 제공 (공고 마감일, 지원금액, 자격 요건)
### 3. 정확한 정보 제공
- 공식 출처의 정보만 사용
- 마감 여부는 KST 기준 현재 날짜와 비교하여 판정
- 공식 사이트 링크 항상 제공
## 구현 방식
### 데이터 흐름
1. **API 요청**: 사용자의 요청을 받아 적절한 API 호출
2. **데이터 수집**: 공공데이터포털, 지자체 API 등에서 병렬 데이터 수집
3. **데이터 처리**: 중복 제거, 정렬, 필터링
4. **정보 제공**: 사용자에게 정제된 정보 제공
### API 엔드포인트
#### k-skill-proxy 라우트
공공데이터포털 K-Startup OpenAPI는 별도 `kstartup-search` 스킬과 `k-skill-proxy``/v1/kstartup/*` 라우트가 담당합니다. `startup-support` helper는 지역별 공개 API 목록을 조회하고, 상세 정보는 결과의 공식 `url` 로 확인합니다.
#### Python 스크립트
```python
# 기본 검색
programs = search_startup_support()
# 지역별 검색
seoul_programs = search_startup_support(region='서울특별시')
# 키워드 검색
keyword_programs = search_startup_support(keyword='청년')
# 마감 임박 검색
deadline_programs = search_startup_support(deadline_only=True)
# 상세 정보는 목록 결과의 공식 url로 확인
```text
GET /v1/kstartup/announcements
```
## 데이터 소스
It maps user-friendly terms onto the K-Startup query fields:
### 1. 공공데이터포털
- **기관**: 중소벤처기업부
- **API**: 스타트업 지원사업 정보
- **인증**: hosted/self-host proxy 운영 서버에서 API 키 주입
- `region` -> `supt_regin`
- `keyword` -> `biz_pbanc_nm`
- `support_type` -> `supt_biz_clsfc`
- `deadline_only` -> `rcrt_prgs_yn=Y`
### 2. 지자체별 사이트
- **서울시**: https://seoulstartup.go.kr
- **경기도**: https://g-startup.kr
- **부산시**: https://busanstartup.kr
- **광주시**: https://startup.gwangju.kr
- **대구시**: https://daegu-startup.kr
The hosted proxy injects the data.go.kr API key. A user running the helper does not need `DATA_GO_KR_API_KEY`.
### 3. 공기업 및 기금
- **중소기업진흥공단**: https://smbs.or.kr
- **기술보증기금**: https://koreatech.or.kr
- **KOTRA**: https://www.kotra.or.kr
## CLI
## 출력 형식
```bash
python3 startup-support/scripts/startup_support.py \
--region 서울특별시 \
--keyword 청년 \
--deadline-only \
--per-page 5 \
--text
```
### 지원사업 목록
Use dry-run when reviewing request construction without network access:
```bash
python3 startup-support/scripts/startup_support.py \
--region 서울특별시 \
--keyword 청년 \
--deadline-only \
--dry-run
```
## Output
The helper normalizes K-Startup announcement rows into:
```json
{
"programs": [
{
"id": "seoul_2024_startup_001",
"title": "서울시 청년 스타트업 창업 지원금",
"organization": "서울시",
"region": "서울특별시",
"support_type": "보조금",
"amount": "최대 5천만원",
"deadline": "2024-12-31",
"target": "만 19~34세 청년 창업가",
"contact": "02-1234-5678",
"url": "https://seoulstartup.go.kr/program/001",
"source": "서울시 창업플러스",
"last_updated": "2024-05-20"
"id": "A1",
"title": "서울 청년 창업 지원",
"organization": "창업진흥원",
"region": "서울",
"support_type": "사업화",
"amount": "공식 공고 확인",
"deadline": "2026-06-30",
"target": "예비창업자",
"contact": "",
"url": "https://www.k-startup.go.kr/...",
"source": "K-Startup",
"last_updated": "2026-06-01"
}
]
}
```
### 상세 정보
```json
{
"program": {
"id": "seoul_2024_startup_001",
"title": "서울시 청년 스타트업 창업 지원금",
"organization": "서울시",
"region": "서울특별시",
"support_type": "보조금",
"amount": "최대 5천만원",
"deadline": "2024-12-31",
"target": "만 19~34세 청년 창업가",
"requirements": [
"사업자등록증 (개인/법인)",
"사업계획서",
"재무제표",
"창업자 신분증"
],
"application_process": [
"온라인 신청서 작성",
"서류 제출",
"서류 심사",
"현장 면접 (일부)",
"결공고"
],
"contact": {
"phone": "02-1234-5678",
"email": "startup@seoul.go.kr",
"address": "서울시 강남구 테헤란로 123"
},
"url": "https://seoulstartup.go.kr/program/001",
"source": "서울시 창업플러스",
"last_updated": "2024-05-20"
}
}
```
## 테스트
### 테스트 실행
## Verification
```bash
cd /startup-support/scripts
python3 test_startup_support.py
python3 -m py_compile startup-support/scripts/startup_support.py startup-support/scripts/test_startup_support.py
PYTHONPATH=startup-support/scripts python3 -m unittest discover -s startup-support/scripts -p 'test_startup_support.py'
python3 startup-support/scripts/startup_support.py --region 서울특별시 --keyword 청년 --deadline-only --dry-run
```
### 테스트 범위
The root `npm run ci` also compiles and runs this helper's tests.
1. **기본 기능 테스트**
- 서울시 지원사업 조회
- 경기도 지원사업 조회
- 전국 지원사업 조회
## Failure Modes
2. **검색 기능 테스트**
- 키워드 검색 ("청년", "MVP", "해외")
- 지역별 검색
- 마감일 순 정렬
3. **에러 처리 테스트**
- API 연결 실패 시 처리
- 데이터 없을 때 처리
- 잘못된 파라미터 처리
## 배포
### 1. 환경 변수 설정
```bash
export DATA_GO_KR_API_KEY="your_api_key_here"
```
### 2. k-skill-proxy 빌드
```bash
cd packages/k-skill-proxy
npm run build
```
### 3. 테스트
```bash
npm test
```
### 4. 배포
```bash
npm run ci
```
## 기여
### 문제 해결
1. **API 연결 실패**: 지자체 API 엔드포인트 확인
2. **데이터 누락**: 공공데이터포털 API 키 확인
3. **성능 문제**: 캐시 시간 조정
4. **정확도 문제**: 출처별 데이터 검증
### 개선 방향
1. **추가 지원사업**: 다른 지자체 API 추가
2. **실시간 업데이트**: 자동 데이터 수집 시스템
3. **사용자 경험**: 검색 결과 개선
4. **모니터링**: 서비스 상태 모니터링
## 참고 자료
- [API 문서](references/api-documentation.md)
- [데이터 출처](references/data-sources.md)
- [지원사업 분류](references/program-categories.md)
- `400 bad_request`: invalid query parameter rejected by `k-skill-proxy`.
- `503 upstream_not_configured`: proxy lacks a configured `DATA_GO_KR_API_KEY`.
- `502 upstream_error` or `upstream_invalid_response`: data.go.kr returned an error, non-JSON body, or invalid payload.
- Empty `programs`: no matching announcements in the selected page. Broaden filters or check additional pages.

24
package-lock.json generated
View file

@ -1762,7 +1762,7 @@
}
},
"packages/court-auction-notice-search": {
"version": "0.2.0",
"version": "0.3.0",
"license": "MIT",
"bin": {
"court-auction-notice-search": "bin/court-auction-notice-search.js"
@ -1776,7 +1776,7 @@
}
},
"packages/daishin-report-search": {
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"bin": {
"daishin-report-search": "src/cli.js"
@ -1786,28 +1786,28 @@
}
},
"packages/daiso-product-search": {
"version": "0.2.0",
"version": "0.5.0",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"packages/donation-place-search": {
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"packages/emergency-room-beds": {
"version": "0.1.0",
"version": "0.3.0",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"packages/gangnamunni-clinic-search": {
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"bin": {
"gangnamunni-clinic-search": "src/cli.js"
@ -1817,7 +1817,7 @@
}
},
"packages/gongsijiga-search": {
"version": "0.1.0",
"version": "0.1.1",
"license": "MIT",
"engines": {
"node": ">=18"
@ -1844,7 +1844,7 @@
}
},
"packages/k-skill-proxy": {
"version": "0.2.0",
"version": "0.5.0",
"license": "MIT",
"dependencies": {
"fastify": "^5.3.3"
@ -1888,7 +1888,7 @@
}
},
"packages/korean-marathon-schedule": {
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"bin": {
"korean-marathon-schedule": "src/cli.js"
@ -1905,7 +1905,7 @@
}
},
"packages/local-election-candidate-search": {
"version": "0.1.0",
"version": "0.3.0",
"license": "MIT",
"bin": {
"local-election-candidate-search": "src/cli.js"
@ -1936,7 +1936,7 @@
}
},
"packages/sh-notice-search": {
"version": "0.1.0",
"version": "0.3.0",
"license": "MIT",
"bin": {
"sh-notice-search": "src/cli.js"
@ -1946,7 +1946,7 @@
}
},
"packages/toss-securities": {
"version": "0.3.0",
"version": "0.4.0",
"license": "MIT",
"engines": {
"node": ">=18"

View file

@ -11,9 +11,9 @@
"build": "npm run build --workspaces --if-present",
"build:manus-bundle": "node scripts/build-manus-bundle.js",
"generate:plugin-manifest": "node scripts/generate-plugin-manifest.js",
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js scripts/korean_middle_korean.js scripts/test_korean_middle_korean.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/generate-plugin-manifest.js scripts/test_generate_plugin_manifest.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/nts_business_registration.py scripts/test_nts_business_registration.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py nts-business-registration/scripts/nts_business_registration.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py scripts/test_ohou_today_deal.py scripts/ticket_availability.py scripts/test_ticket_availability.py scripts/test_danawa_price_search.py ticket-availability/scripts/ticket_availability.py coupang-product-search/scripts/coupang_partners_mcp.py ohou-today-deal/scripts/ohou_today_deal.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py seoul-bike/scripts/seoul_bike.py scripts/test_seoul_bike.py danawa-price-search/scripts/danawa_search.py kosis-stats/scripts/run_kosis_stats.py kosis-stats/tests/test_run_kosis_stats.py kstartup-search/scripts/run_kstartup.py kstartup-search/tests/test_run_kstartup.py intercity-bus-booking/scripts/intercity_bus_search.py daangn-used-goods-search/scripts/daangn_used_goods.py daangn-realty-search/scripts/daangn_realty.py daangn-jobs-search/scripts/daangn_jobs.py daangn-cars-search/scripts/daangn_cars.py foresttrip-vacancy/scripts/run_foresttrip_vacancy.py foresttrip-vacancy/tests/test_run_foresttrip_vacancy.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh && node scripts/generate-plugin-manifest.js --check",
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js scripts/korean_middle_korean.js scripts/test_korean_middle_korean.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/generate-plugin-manifest.js scripts/test_generate_plugin_manifest.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/nts_business_registration.py scripts/test_nts_business_registration.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py nts-business-registration/scripts/nts_business_registration.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py scripts/test_ohou_today_deal.py scripts/ticket_availability.py scripts/test_ticket_availability.py scripts/test_danawa_price_search.py ticket-availability/scripts/ticket_availability.py coupang-product-search/scripts/coupang_partners_mcp.py ohou-today-deal/scripts/ohou_today_deal.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py seoul-bike/scripts/seoul_bike.py scripts/test_seoul_bike.py danawa-price-search/scripts/danawa_search.py kosis-stats/scripts/run_kosis_stats.py kosis-stats/tests/test_run_kosis_stats.py kstartup-search/scripts/run_kstartup.py kstartup-search/tests/test_run_kstartup.py intercity-bus-booking/scripts/intercity_bus_search.py daangn-used-goods-search/scripts/daangn_used_goods.py daangn-realty-search/scripts/daangn_realty.py daangn-jobs-search/scripts/daangn_jobs.py daangn-cars-search/scripts/daangn_cars.py foresttrip-vacancy/scripts/run_foresttrip_vacancy.py foresttrip-vacancy/tests/test_run_foresttrip_vacancy.py startup-support/scripts/startup_support.py startup-support/scripts/test_startup_support.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh && node scripts/generate-plugin-manifest.js --check",
"typecheck": "tsc --noEmit",
"test": "python3 -m pip install --user --quiet beautifulsoup4 && node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js scripts/test_korean_middle_korean.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/test_generate_plugin_manifest.js && PYTHONPATH=.:scripts python3 -m unittest scripts.test_k_skill_cleaner scripts.test_fine_dust scripts.test_ktx_booking scripts.test_sillok_search scripts.test_korean_spell_check scripts.test_patent_search scripts.test_mfds_drug_safety scripts.test_nts_business_registration scripts.test_mfds_food_safety scripts.test_zipcode_search scripts.test_subway_lost_property scripts.test_geeknews_search scripts.test_naver_blog_search scripts.test_korean_slang_writing scripts.test_kakaotalk_mac scripts.test_coupang_partners_mcp_wrapper scripts.test_ohou_today_deal scripts.test_ticket_availability scripts.test_seoul_bike scripts.test_danawa_price_search && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && PYTHONPATH=.:scripts:kosis-stats/scripts python3 -m unittest discover -s kosis-stats/tests -p 'test_run_kosis_stats.py' && PYTHONPATH=.:scripts:kstartup-search/scripts python3 -m unittest discover -s kstartup-search/tests -p 'test_run_kstartup.py' && PYTHONPATH=.:foresttrip-vacancy/scripts python3 -m unittest discover -s foresttrip-vacancy/tests -p 'test_run_foresttrip_vacancy.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
"test": "python3 -m pip install --user --quiet beautifulsoup4 && node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js scripts/test_korean_middle_korean.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/test_generate_plugin_manifest.js && PYTHONPATH=.:scripts python3 -m unittest scripts.test_k_skill_cleaner scripts.test_fine_dust scripts.test_ktx_booking scripts.test_sillok_search scripts.test_korean_spell_check scripts.test_patent_search scripts.test_mfds_drug_safety scripts.test_nts_business_registration scripts.test_mfds_food_safety scripts.test_zipcode_search scripts.test_subway_lost_property scripts.test_geeknews_search scripts.test_naver_blog_search scripts.test_korean_slang_writing scripts.test_kakaotalk_mac scripts.test_coupang_partners_mcp_wrapper scripts.test_ohou_today_deal scripts.test_ticket_availability scripts.test_seoul_bike scripts.test_danawa_price_search && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && PYTHONPATH=.:scripts:kosis-stats/scripts python3 -m unittest discover -s kosis-stats/tests -p 'test_run_kosis_stats.py' && PYTHONPATH=.:scripts:kstartup-search/scripts python3 -m unittest discover -s kstartup-search/tests -p 'test_run_kstartup.py' && PYTHONPATH=.:foresttrip-vacancy/scripts python3 -m unittest discover -s foresttrip-vacancy/tests -p 'test_run_foresttrip_vacancy.py' && PYTHONPATH=startup-support/scripts python3 -m unittest discover -s startup-support/scripts -p 'test_startup_support.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
"pack:dry-run": "npm pack --workspace k-lotto --dry-run && npm pack --workspace daiso-product-search --dry-run && npm pack --workspace market-kurly-search --dry-run && npm pack --workspace blue-ribbon-nearby --dry-run && npm pack --workspace kakao-bar-nearby --dry-run && npm pack --workspace cheap-gas-nearby --dry-run && npm pack --workspace public-restroom-nearby --dry-run && npm pack --workspace parking-lot-search --dry-run && npm pack --workspace court-auction-notice-search --dry-run && npm pack --workspace donation-place-search --dry-run && npm pack --workspace gongsijiga-search --dry-run && npm pack --workspace kbl-results --dry-run && npm pack --workspace kleague-results --dry-run && npm pack --workspace lck-analytics --dry-run && npm pack --workspace toss-securities --dry-run && npm pack --workspace hipass-receipt --dry-run && npm pack --workspace used-car-price-search --dry-run && npm pack --workspace k-skill-rhwp --dry-run && npm pack --workspace korean-marathon-schedule --dry-run && npm pack --workspace gangnamunni-clinic-search --dry-run && npm pack --workspace daishin-report-search --dry-run && npm pack --workspace sh-notice-search --dry-run && npm pack --workspace emergency-room-beds --dry-run && npm pack --workspace local-election-candidate-search --dry-run",
"ci": "npm run lint && npm run typecheck && npm run test && npm run pack:dry-run",
"version-packages": "changeset version",

View file

@ -5132,5 +5132,3 @@ module.exports = {
resolveLatestKmaForecastBase,
startServer
};

View file

@ -1,6 +1,6 @@
---
name: startup-support
description: Search Korean government startup support programs, grants, and subsidies for startups, SMEs, and entrepreneurs through various public APIs. Use when users ask about 창업 지원, 스타트업 지원금, 중소기업 지원, 정부 지원사업.
description: Search Korean K-Startup government startup support announcements through k-skill-proxy. Use when users ask about 창업 지원, 스타트업 지원금, 중소기업 지원, 정부 지원사업, or 모집 중인 창업 공고.
license: MIT
metadata:
category: business-support
@ -10,177 +10,104 @@ metadata:
# 스타트업 지원사업 조회
## What this skill does
## What This Skill Does
정부, 지자체, 공기업이 제공하는 **스타트업, 중소기업, 개인 창업가**를 위한 지원사업, 보조금, 융자 프로그램, 멘토링, 교육 프로그램 등을 공식 API를 통해 조회한다.
`startup-support` searches K-Startup announcement data through the hosted or self-hosted `k-skill-proxy` `/v1/kstartup/announcements` route and summarizes matching startup support programs.
본 스킬은 창업 준비 중이거나, 사업 초기 단계에 있는 스타트업 창업가가 **어떤 지원사업이 있는지 빠르게 파악**하고, 지원 가능한 자격 요건을 확인하며, 공고 마감일을 추적할 수 있도록 돕는다.
Use it for:
## When to use
- "서울 청년 창업 지원사업 알려줘"
- "모집 중인 사업화 지원 공고 찾아줘"
- "정부 창업 지원사업 마감일 확인해줘"
- "예비창업자 대상 K-Startup 공고 요약해줘"
- "스타트업 지원사업 알려줘"
- "중소기업 보조금 종류 정리해줘"
- "서울시 창업 지원금 프로그램"
- "청년 창업 지원금 요건"
- "MVP 지원사업 목록"
- "정부 지원사업 마감일"
- "스타트업 융자 프로그램"
Do not use it for:
## When not to use
- Application submission or account/payment automation
- Final legal eligibility decisions
- Inventing requirements, award amounts, contacts, or application steps not present in upstream data
- Local-government site crawling outside K-Startup
- 개별 지원금 신청 자동화 (본 스킬은 read-only 조회다)
- 세무/회계 절차 대체 (전문가 상담 필요)
- 법적 자격 심사 결정
- 실제 지급액 계산 (정확한 금액은 공식 사이트 확인)
- 창업아이디어 구체화 컨설팅
## Data Source And Credentials
## Core principle
- Source: 공공데이터포털 창업진흥원 K-Startup 조회서비스 (`15125364`)
- Proxy route: `/v1/kstartup/announcements`
- User credential requirement: none for normal hosted-proxy use
- Proxy operator credential: `DATA_GO_KR_API_KEY`
1. **공식 API 우선**: 정부 공공데이터포털(`data.go.kr`), 각 지자체 API, 공기업 API 등 공식 출처를 사용
2. **전수 검색**: 모든 소스를 병렬로 검색하여 누락되는 지원사업이 없도록 함
3. **실시간 정보**: 공고 마감일, 지원금액, 자격 요건 등 최신 정보만 제공
4. **정확한 한글**: 전문 용어를 정확히 표기 (예: "MVP 지원" vs "MVP지원")
Set `KSKILL_PROXY_BASE_URL` or pass `--proxy-base-url` only when using a self-host proxy. For direct data.go.kr calls with a user key, use `kstartup-search` and its `--direct` mode.
## Implementation
## Workflow
### Data Sources
1. Translate the user request into K-Startup announcement filters.
2. Run a small bounded search first.
3. Check each returned row's official `url` before making eligibility-sensitive claims.
4. Cite the official URL when summarizing.
1. **공공데이터포털 (data.go.kr)**
- 중소벤처기업부 스타트업 지원사업 API
- 서울시 창업 지원 프로그램 API
- 각 지자체 창업 지원사업 API
Common filter mapping:
2. **지자체별 공식 사이트**
- 서울시 창업플러스 (seoulstartup.go.kr)
- 경기도 창업진흥원 (g-startup.kr)
- 부산시 스타트업 허브 (busanstartup.kr)
- 광주창업파크 (startup.gwangju.kr)
- 대구창업진흥원 (daegu-startup.kr)
- region -> `supt_regin`
- keyword -> `biz_pbanc_nm`
- support type -> `supt_biz_clsfc`
- deadline-only / recruiting-only -> `rcrt_prgs_yn=Y`
3. **공기업 및 기금 관리기관**
- 중소기업진흥공단 (smbs.or.kr)
- 기술보증기금 (koreatech.or.kr)
- KOTRA 해외진출 지원
- 중소벤처기업금융공단
## CLI
### Proxy Integration
```bash
python3 scripts/startup_support.py \
--region 서울특별시 \
--keyword 청년 \
--deadline-only \
--per-page 5 \
--text
```
공공데이터포털 K-Startup OpenAPI는 `kstartup-search` 스킬과 `k-skill-proxy``/v1/kstartup/*` 라우트가 담당한다. 이 스킬의 helper는 지역별 공개 API 목록을 조회하고, 상세 정보는 결과의 공식 `url` 로 확인한다.
Dry-run request construction without network or credentials:
## Output format
```bash
python3 scripts/startup_support.py \
--region 서울특별시 \
--keyword 청년 \
--deadline-only \
--dry-run
```
### 지원사업 목록
## Output
The helper returns:
```json
{
"programs": [
{
"id": "seoul_2024_startup_001",
"title": "서울 청년 스타트업 창업 지원",
"organization": "서울시",
"region": "서울특별시",
"support_type": "보조금",
"amount": "최대 5천만원",
"deadline": "2024-12-31",
"target": "만 19~34세 청년 창업가",
"contact": "02-1234-5678",
"url": "https://seoulstartup.go.kr/program/001",
"source": "서울시 창업플러스",
"last_updated": "2024-05-20"
"id": "A1",
"title": "서울 청년 창업 지원",
"organization": "창업진흥원",
"region": "서울",
"support_type": "사업화",
"amount": "공식 공고 확인",
"deadline": "2026-06-30",
"target": "예비창업자",
"contact": "",
"url": "https://www.k-startup.go.kr/...",
"source": "K-Startup",
"last_updated": "2026-06-01"
}
]
}
```
### 특정 지원사업 상세 정보
## Failure Modes
```json
{
"program": {
"id": "seoul_2024_startup_001",
"title": "서울시 청년 스타트업 창업 지원금",
"organization": "서울시",
"region": "서울특별시",
"support_type": "보조금",
"amount": "최대 5천만원",
"deadline": "2024-12-31",
"target": "만 19~34세 청년 창업가",
"requirements": [
"사업자등록증 (개인/법인)",
"사업계획서",
"재무제표",
"창업자 신분증"
],
"application_process": [
"온라인 신청서 작성",
"서류 제출",
"서류 심사",
"현장 면접 (일부)",
"결공고"
],
"contact": {
"phone": "02-1234-5678",
"email": "startup@seoul.go.kr",
"address": "서울시 강남구 테헤란로 123"
},
"url": "https://seoulstartup.go.kr/program/001",
"source": "서울시 창업플러스",
"last_updated": "2024-05-20"
}
}
- Empty result page: broaden filters or inspect additional pages.
- `400 bad_request`: invalid query rejected by proxy validation.
- `503 upstream_not_configured`: proxy lacks `DATA_GO_KR_API_KEY`.
- `502 upstream_error` or invalid response: data.go.kr returned an upstream error or non-JSON body.
## Maintainer Checks
```bash
python3 -m py_compile startup-support/scripts/startup_support.py startup-support/scripts/test_startup_support.py
PYTHONPATH=startup-support/scripts python3 -m unittest discover -s startup-support/scripts -p 'test_startup_support.py'
python3 startup-support/scripts/startup_support.py --region 서울특별시 --keyword 청년 --deadline-only --dry-run
```
## Testing
### 테스트 케이스
1. **기본 기능 테스트**
- 서울시 지원사업 조회
- 경기도 지원사업 조회
- 전국 지원사업 조회
2. **검색 기능 테스트**
- 키워드 검색 ("청년", "MVP", "해외")
- 지역별 검색
- 마감일 순 정렬
3. **에러 처리 테스트**
- API 연결 실패 시 처리
- 데이터 없을 때 처리
- 잘못된 파라미터 처리
### 테스트 데이터
테스트 시 다음과 같은 가상 데이터를 사용:
```json
{
"test_programs": [
{
"id": "test_001",
"title": "테스트 스타트업 지원사업",
"organization": "테스트 기관",
"region": "테스트 지역",
"support_type": "보조금",
"amount": "최대 1천만원",
"deadline": "2024-12-31",
"target": "테스트 대상",
"contact": "02-1234-5678",
"url": "https://test.example.com",
"source": "테스트 소스",
"last_updated": "2024-05-20"
}
]
}
```
## Files
- `SKILL.md` - 이 문서
- `scripts/` - Python 스크립트 구현
- `startup_support.py` - 메인 로직
- `test_startup_support.py` - 테스트 파일
- `references/` - 참고 자료
- `api-documentation.md` - API 문서
- `data-sources.md` - 데이터 출처
- `program-categories.md` - 지원사업 분류

View file

@ -1,68 +1,54 @@
# 스타트업 지원사업 API 문서
# startup-support API surface
## API 엔드포인트
`startup-support` is a read-only helper over the existing K-Startup proxy route. It does not add independent proxy endpoints.
### k-skill-proxy 라우트
## Proxy route used
#### 목록 조회
```
GET /v1/startup-support/list
```text
GET /v1/kstartup/announcements
```
파라미터:
- `region`: 지역 (선택)
- `keyword`: 검색 키워드 (선택)
- `support_type`: 지원 유형 (선택)
- `deadline_only`: 마감 임박만 검색 (선택)
Common query mapping:
#### 상세 조회
```
GET /v1/startup-support/detail/:program_id
```
- `region` -> `supt_regin`
- `keyword` -> `biz_pbanc_nm`
- `support_type` -> `supt_biz_clsfc`
- `deadline_only=true` -> `rcrt_prgs_yn=Y`
- `page` -> `page`
- `per_page` -> `perPage`
#### 지역별 조회
```
GET /v1/startup-support/region/:region
```
Authentication is handled by hosted or self-hosted `k-skill-proxy`. Users do not pass a data.go.kr service key to this helper.
#### 마감 임박 조회
```
GET /v1/startup-support/deadline
```
## Python API
### 클래스 구조
## Python helper
```python
class StartupSupportAPI:
def __init__(self)
def search_programs(self, region, keyword, support_type, deadline_only)
def get_program_detail(self, program_id)
def _search_data_go_kr(self, region, keyword, support_type)
def _search_by_region(self, region, keyword, support_type)
def _parse_program_from_data_go_kr(self, item)
def _parse_program_from_region_api(self, item, region)
def _filter_upcoming_deadline(self, programs)
def _remove_duplicates(self, programs)
def _sort_programs(self, programs)
programs = search_startup_support(
region="서울특별시",
keyword="청년",
support_type="사업화",
deadline_only=True,
)
```
### 사용 예제
The CLI exposes the same search:
```python
# 기본 검색
programs = search_startup_support()
# 지역별 검색
seoul_programs = search_startup_support(region='서울특별시')
# 키워드 검색
keyword_programs = search_startup_support(keyword='청년')
# 마감 임박 검색
deadline_programs = search_startup_support(deadline_only=True)
# 상세 정보 조회
detail = get_startup_program_detail('test_001')
```bash
python3 startup-support/scripts/startup_support.py \
--region 서울특별시 \
--keyword 청년 \
--deadline-only \
--per-page 5 \
--text
```
For request inspection without network or credentials:
```bash
python3 startup-support/scripts/startup_support.py \
--region 서울특별시 \
--keyword 청년 \
--deadline-only \
--dry-run
```
Detailed eligibility and application steps must be confirmed from each result's official `url`.

View file

@ -1,122 +1,28 @@
# 스타트업 지원사업 데이터 출처
# startup-support data sources
## 1. 공공데이터포털 (data.go.kr)
## Primary Source
### API 정보
- **기관**: 중소벤처기업부
- **서비스명**: 스타트업 지원사업 정보
- **API URL**: https://www.data.go.kr/api/15058530/openapi
- **인증**: API 키 필수 (DATA_GO_KR_API_KEY 환경 변수)
- K-Startup announcement data through `k-skill-proxy` `/v1/kstartup/announcements`
- Upstream dataset: 공공데이터포털 창업진흥원 K-Startup 조회서비스 (`15125364`)
- Authentication: proxy server injects `DATA_GO_KR_API_KEY`
- Update cadence: official K-Startup/data.go.kr feed cadence, not realtime monitoring
### 데이터 구조
```json
{
"items": [
{
"pan_id": "공고ID",
"pan_nm": "공고명",
"cnp_cd_nm": "지역명",
"support_type": "지원 유형",
"amount": "지원 금액",
"clsg_dt": "마감일",
"target": "대상",
"contact": "연락처",
"detail_url": "상세 URL"
}
]
}
```
## Helper Scope
### 사용 예제
```python
response = requests.get(url, params=params, headers=headers)
data = response.json()
```
The helper searches announcement rows and returns the official detail URL for each result. It does not crawl local-government sites directly and does not synthesize eligibility, required documents, contacts, or award amounts when upstream data omits them.
## 2. 지자체별 공식 사이트
## Fallback Order
### 서울시 창업플러스
- **URL**: https://seoulstartup.go.kr
- **API 엔드포인트**: https://seoulstartup.go.kr/api/program/list
- **특징**: 서울시 내 스타트업 지원사업 전체
1. Hosted proxy from `KSKILL_PROXY_BASE_URL` or `https://k-skill-proxy.nomadamas.org`
2. User-provided self-host proxy via `--proxy-base-url`
3. Dry-run URL inspection when network, proxy configuration, or upstream credentials are unavailable
### 경기도 창업진흥원
- **URL**: https://g-startup.kr
- **API 엔드포인트**: https://g-startup.kr/api/support/list
- **특징**: 경기도 내 스타트업 지원사업
For direct data.go.kr calls with a user-held key, use the narrower `kstartup-search` helper's `--direct` mode.
### 부산시 스타트업 허브
- **URL**: https://busanstartup.kr
- **API 엔드포인트**: https://busanstartup.kr/api/program/list
- **특징**: 부산시 내 스타트업 지원사업
## Failure Modes
### 광주창업파크
- **URL**: https://startup.gwangju.kr
- **API 엔드포인트**: https://startup.gwangju.kr/api/support/list
- **특징**: 광주시 내 스타트업 지원사업
### 대구창업진흥원
- **URL**: https://daegu-startup.kr
- **API 엔드포인트**: https://daegu-startup.kr/api/program/list
- **특징**: 대구시 내 스타트업 지원사업
## 3. 공기업 및 기금 관리기관
### 중소기업진흥공단 (SMBS)
- **URL**: https://smbs.or.kr
- **제공 서비스**: 중소기업 지원금, 융자 프로그램
- **API**: 공공데이터포털 통합
### 기술보증기금
- **URL**: https://koreatech.or.kr
- **제공 서비스**: 기술 기반 스타트업 보증 지원
- **API**: 공공데이터포털 통합
### KOTRA
- **URL**: https://www.kotra.or.kr
- **제공 서비스**: 해외 진출 지원사업
- **API**: 별도 API 제공
### 중소벤처기업금융공단
- **URL**: https://www.sbc.or.kr
- **제공 서비스**: 스타트업 투자, 융자
- **API**: 공공데이터포털 통합
## 4. 데이터 통합 방식
### 1단계: API 호출
- 공공데이터포털 API 호출
- 지자체별 API 병렬 호출
- 공기업 API 호출 (필요 시)
### 2단계: 데이터 파싱
- 각 API 응답 구조에 맞게 데이터 추출
- 필수 필드 검증 (ID, 제목, 지역, 마감일 등)
- 데이터 정규화 (지역명, 지원 유형 표준화)
### 3단계: 중복 제거
- ID 기준 중복 제거
- 동일 지원사업 합치기
- 최신 정보 유지
### 4단계: 정렬
- 마감일 기준 정렬 (가까운 순)
- 지역별 그룹화
- 지원 유형별 분류
## 5. 데이터 업데이트 전략
### 주기적 업데이트
- 공공데이터포털: 매일 2회 (09:00, 15:00)
- 지자체별: 매일 1회 (09:00)
- 공기업: 주간 업데이트
### 즉시 업데이트
- 새 공고 등록 시
- 마감일 변경 시
- 지원 조건 변경 시
### 캐시 정책
- API 응답: 1시간 캐시
- 데이터 저장: 24시간 캐시
- 최종 결과: 5분 캐시
- Empty result page: no matching K-Startup announcements for the selected filters/page.
- HTTP 400: invalid query accepted by the helper but rejected by proxy validation.
- HTTP 503: proxy has no configured `DATA_GO_KR_API_KEY`.
- HTTP 502: upstream data.go.kr error or invalid response.
- Missing detail fields: answer with the returned official URL instead of inventing requirements, amounts, or contacts.

View file

@ -1,334 +1,283 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import requests
import time
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import os
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Sequence
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
REGION_ALIASES = {
"서울특별시": "서울",
"부산광역시": "부산",
"대구광역시": "대구",
"인천광역시": "인천",
"광주광역시": "광주",
"대전광역시": "대전",
"울산광역시": "울산",
"세종특별자치시": "세종",
"경기도": "경기",
"강원특별자치도": "강원",
"강원도": "강원",
"충청북도": "충북",
"충청남도": "충남",
"전북특별자치도": "전북",
"전라북도": "전북",
"전라남도": "전남",
"경상북도": "경북",
"경상남도": "경남",
"제주특별자치도": "제주",
}
class HelperError(RuntimeError):
pass
def _compact(value: str | None) -> str | None:
if value is None:
return None
text = value.strip()
return text or None
def _yyyymmdd_to_iso(value: str | None) -> str:
if not value:
return ""
digits = "".join(char for char in value if char.isdigit())
if len(digits) != 8:
return value
return f"{digits[0:4]}-{digits[4:6]}-{digits[6:8]}"
def build_query(args: argparse.Namespace) -> dict[str, str | int]:
if args.page < 1:
raise HelperError("--page must be >= 1")
if args.per_page < 1 or args.per_page > 100:
raise HelperError("--per-page must be in [1, 100]")
query: dict[str, str | int] = {
"page": args.page,
"perPage": args.per_page,
"returnType": "json",
}
region = _compact(args.region)
keyword = _compact(args.keyword)
support_type = _compact(args.support_type)
if region and region != "전국":
query["supt_regin"] = region
if keyword:
query["biz_pbanc_nm"] = keyword
if support_type:
query["supt_biz_clsfc"] = support_type
if args.deadline_only:
query["rcrt_prgs_yn"] = "Y"
return query
def build_url(query: dict[str, str | int], proxy_base_url: str = DEFAULT_PROXY_BASE_URL) -> str:
base = proxy_base_url.rstrip("/")
encoded = urllib.parse.urlencode([(key, str(value)) for key, value in query.items()])
return f"{base}/v1/kstartup/announcements?{encoded}"
def http_get(url: str, *, timeout: int) -> tuple[int, str, str]:
request = urllib.request.Request(
url,
headers={
"accept": "application/json",
"user-agent": "k-skill/startup-support",
},
method="GET",
)
context = ssl.create_default_context()
try:
with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
body = response.read().decode("utf-8", errors="replace")
return response.status, response.headers.get("content-type", ""), body
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
content_type = exc.headers.get("content-type", "") if exc.headers else ""
return exc.code, content_type, body
except urllib.error.URLError as exc:
raise HelperError(f"network error: {exc.reason}") from exc
def _rows_from_payload(payload: dict[str, object]) -> list[dict[str, object]]:
rows = payload.get("data", [])
if not isinstance(rows, list):
raise HelperError("proxy response data must be a list")
normalized: list[dict[str, object]] = []
for row in rows:
if isinstance(row, dict):
normalized.append(row)
return normalized
def _text(row: dict[str, object], *keys: str) -> str:
for key in keys:
value = row.get(key)
if value is not None and str(value).strip():
return str(value).strip()
return ""
def _canonical_region(value: str | None) -> str:
text = _compact(value)
if not text:
return ""
compacted = text.replace(" ", "")
return REGION_ALIASES.get(compacted, compacted)
def _region_parts(value: str) -> list[str]:
return [
_canonical_region(part)
for part in value.replace("/", ",").replace("|", ",").split(",")
if _canonical_region(part)
]
def _matches_region(row: dict[str, object], requested_region: str | None) -> bool:
requested = _canonical_region(requested_region)
if not requested or requested == "전국":
return True
row_region = _text(row, "supt_regin", "region")
if not row_region:
return False
return requested in _region_parts(row_region)
def _program_from_row(row: dict[str, object]) -> dict[str, str]:
program_id = _text(row, "pbanc_sn", "id")
title = _text(row, "biz_pbanc_nm", "title")
end_date = _text(row, "pbanc_rcpt_end_dt", "deadline")
return {
"id": program_id,
"title": title,
"organization": _text(row, "sprv_inst", "organization"),
"region": _text(row, "supt_regin", "region", "전국"),
"support_type": _text(row, "supt_biz_clsfc", "support_type", "기타"),
"amount": _text(row, "supt_cn", "amount", "공식 공고 확인"),
"deadline": _yyyymmdd_to_iso(end_date),
"target": _text(row, "aply_trgt", "target"),
"contact": _text(row, "biz_gdnc_url", "contact"),
"url": _text(row, "detl_pg_url", "url"),
"source": "K-Startup",
"last_updated": _yyyymmdd_to_iso(_text(row, "pbanc_rcpt_bgng_dt", "last_updated")),
}
def search_startup_support(
region: str = "전국",
keyword: str | None = None,
support_type: str | None = None,
deadline_only: bool = False,
*,
page: int = 1,
per_page: int = 10,
proxy_base_url: str | None = None,
timeout: int = 30,
) -> list[dict[str, str]]:
args = argparse.Namespace(
region=region,
keyword=keyword,
support_type=support_type,
deadline_only=deadline_only,
page=page,
per_page=per_page,
)
base_url = proxy_base_url or os.environ.get("KSKILL_PROXY_BASE_URL", DEFAULT_PROXY_BASE_URL)
url = build_url(build_query(args), proxy_base_url=base_url)
status, _, body = http_get(url, timeout=timeout)
if status < 200 or status >= 300:
raise HelperError(f"proxy returned HTTP {status}: {body[:300]}")
try:
payload = json.loads(body)
except json.JSONDecodeError as exc:
raise HelperError("proxy response was not valid JSON") from exc
if not isinstance(payload, dict):
raise HelperError("proxy response must be a JSON object")
rows = [row for row in _rows_from_payload(payload) if _matches_region(row, region)]
return [_program_from_row(row) for row in rows]
def get_startup_program_detail(program_id: str) -> None:
return None
class StartupSupportAPI:
"""스타트업 지원사업 API 클라이언트"""
def search_programs(
self,
region: str = "전국",
keyword: str | None = None,
support_type: str | None = None,
deadline_only: bool = False,
) -> list[dict[str, str]]:
return search_startup_support(region, keyword, support_type, deadline_only)
def __init__(self):
self.base_urls = {
'seoul': 'https://seoulstartup.go.kr',
'gyeonggi': 'https://g-startup.kr',
'busan': 'https://busanstartup.kr',
'gwangju': 'https://startup.gwangju.kr',
'daegu': 'https://daegu-startup.kr',
'nationwide': 'https://www.data.go.kr'
}
def get_program_detail(self, program_id: str) -> None:
return get_startup_program_detail(program_id)
# 공공데이터포털 API 키 (환경 변수에서 가져오기)
self.data_go_kr_api_key = os.getenv('DATA_GO_KR_API_KEY')
# 헤더 설정
self.headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
def _print_text(programs: Sequence[dict[str, str]]) -> None:
if not programs:
print("일치하는 K-Startup 지원사업 공고가 없습니다.")
return
for index, program in enumerate(programs, start=1):
deadline = program["deadline"] or "마감일 공고 확인"
url = program["url"] or "상세 URL 없음"
print(f"{index}. {program['title']} | {program['region']} | {deadline}")
print(f" {url}")
def search_programs(self, region: str = '전국', keyword: Optional[str] = None,
support_type: Optional[str] = None, deadline_only: bool = False) -> List[Dict]:
"""
지원사업 검색
Args:
region: 지역 (서울특별시, 경기도, 부산광역시 )
keyword: 검색 키워드
support_type: 지원 유형 (보조금, 융자, 멘토링 )
deadline_only: 마감 임박 사업만 검색
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="K-Startup 창업 지원사업 공고 조회")
parser.add_argument("--region", default="전국")
parser.add_argument("--keyword")
parser.add_argument("--support-type")
parser.add_argument("--deadline-only", action="store_true")
parser.add_argument("--page", type=int, default=1)
parser.add_argument("--per-page", type=int, default=10)
parser.add_argument("--text", action="store_true")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--timeout", type=int, default=30)
parser.add_argument("--proxy-base-url", default=os.environ.get("KSKILL_PROXY_BASE_URL", DEFAULT_PROXY_BASE_URL))
return parser
Returns:
지원사업 목록
"""
programs = []
# 1. 공공데이터포털 API 호출
if self.data_go_kr_api_key:
data_go_kr_programs = self._search_data_go_kr(region, keyword, support_type)
programs.extend(data_go_kr_programs)
def run(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
query = build_query(args)
url = build_url(query, proxy_base_url=args.proxy_base_url)
if args.dry_run:
print(json.dumps({"operation": "announcements", "query": query, "url": url}, ensure_ascii=False, indent=2))
return 0
programs = search_startup_support(
region=args.region,
keyword=args.keyword,
support_type=args.support_type,
deadline_only=args.deadline_only,
page=args.page,
per_page=args.per_page,
proxy_base_url=args.proxy_base_url,
timeout=args.timeout,
)
except HelperError as exc:
parser.error(str(exc))
if args.text:
_print_text(programs)
else:
print(json.dumps({"programs": programs}, ensure_ascii=False, indent=2))
return 0
# 2. 지자체별 API 호출
region_programs = self._search_by_region(region, keyword, support_type)
programs.extend(region_programs)
# 3. 마감 임박 필터링
if deadline_only:
programs = self._filter_upcoming_deadline(programs)
# 중복 제거
programs = self._remove_duplicates(programs)
# 정렬
programs = self._sort_programs(programs)
return programs
def _search_data_go_kr(self, region: str, keyword: Optional[str], support_type: Optional[str]) -> List[Dict]:
"""공공데이터포털 API로 검색"""
programs = []
try:
# 중소벤처기업부 스타트업 지원사업 API
url = "https://www.data.go.kr/api/15058530/openapi"
params = {
'serviceKey': self.data_go_kr_api_key,
'pageNo': '1',
'numOfRows': '100',
'_type': 'json'
}
if region and region != '전국':
params['cnpCdNm'] = region
if keyword:
params['panNm'] = keyword
response = requests.get(url, params=params, headers=self.headers, timeout=10)
if response.status_code == 200:
data = response.json()
# 실제 API 응답 구조에 따라 데이터 추출
if 'items' in data:
for item in data['items']:
program = self._parse_program_from_data_go_kr(item)
if program:
programs.append(program)
except Exception as e:
print(f"공공데이터포털 API 오류: {e}")
return programs
def _search_by_region(self, region: str, keyword: Optional[str], support_type: Optional[str]) -> List[Dict]:
"""지자체별 API로 검색"""
programs = []
# 지자체별 API 엔드포인트
region_apis = {
'서울특별시': {
'url': 'https://seoulstartup.go.kr/api/program/list',
'method': 'GET'
},
'경기도': {
'url': 'https://g-startup.kr/api/support/list',
'method': 'GET'
},
'부산광역시': {
'url': 'https://busanstartup.kr/api/program/list',
'method': 'GET'
},
'광주광역시': {
'url': 'https://startup.gwangju.kr/api/support/list',
'method': 'GET'
},
'대구광역시': {
'url': 'https://daegu-startup.kr/api/program/list',
'method': 'GET'
}
}
target_regions = list(region_apis) if region == '전국' else [region]
for target_region in target_regions:
if target_region not in region_apis:
continue
api_info = region_apis[target_region]
try:
params = {}
if keyword:
params['keyword'] = keyword
if support_type:
params['type'] = support_type
response = requests.get(api_info['url'], params=params,
headers=self.headers, timeout=10)
if response.status_code == 200:
data = response.json()
# 실제 API 응답 구조에 따라 데이터 추출
if 'programs' in data:
for item in data['programs']:
program = self._parse_program_from_region_api(item, target_region)
if program:
programs.append(program)
except Exception as e:
print(f"{target_region} API 오류: {e}")
return programs
def _parse_program_from_data_go_kr(self, item: Dict) -> Optional[Dict]:
"""공공데이터포털 응답 파싱"""
try:
program = {
'id': f"data_gov_{item.get('pan_id', '')}",
'title': item.get('pan_nm', ''),
'organization': '중소벤처기업부',
'region': item.get('cnp_cd_nm', '전국'),
'support_type': item.get('support_type', '기타'),
'amount': item.get('amount', '정보 없음'),
'deadline': item.get('clsg_dt', ''),
'target': item.get('target', '전체 대상'),
'contact': item.get('contact', '02-1234-5678'),
'url': item.get('detail_url', ''),
'source': '공공데이터포털',
'last_updated': item.get('last_updated', datetime.now().strftime('%Y-%m-%d'))
}
# 필수 필드 검증
if not program['title']:
return None
return program
except Exception as e:
print(f"공공데이터포털 데이터 파싱 오류: {e}")
return None
def _parse_program_from_region_api(self, item: Dict, region: str) -> Optional[Dict]:
"""지자체 API 응답 파싱"""
try:
program = {
'id': f"{region}_{item.get('id', '')}",
'title': item.get('title', ''),
'organization': region + ' 창업진흥원',
'region': region,
'support_type': item.get('type', '기타'),
'amount': item.get('amount', '정보 없음'),
'deadline': item.get('deadline', ''),
'target': item.get('target', '전체 대상'),
'contact': item.get('contact', '02-1234-5678'),
'url': item.get('url', ''),
'source': region + ' 창업진흥원',
'last_updated': item.get('last_updated', datetime.now().strftime('%Y-%m-%d'))
}
# 필수 필드 검증
if not program['title']:
return None
return program
except Exception as e:
print(f"지자체 API 데이터 파싱 오류: {e}")
return None
def _filter_upcoming_deadline(self, programs: List[Dict]) -> List[Dict]:
"""마감 임박 사업 필터링"""
today = datetime.now()
upcoming_threshold = today + timedelta(days=7) # 7일 이내
filtered = []
for program in programs:
if program['deadline']:
try:
deadline = datetime.strptime(program['deadline'], '%Y-%m-%d')
if today <= deadline <= upcoming_threshold:
filtered.append(program)
except:
# 날짜 파싱 실패 시 제외
continue
return filtered
def _remove_duplicates(self, programs: List[Dict]) -> List[Dict]:
"""중복 제거"""
seen_ids = set()
unique_programs = []
for program in programs:
program_id = program['id']
if program_id not in seen_ids:
seen_ids.add(program_id)
unique_programs.append(program)
return unique_programs
def _sort_programs(self, programs: List[Dict]) -> List[Dict]:
"""사업 정렬"""
# 마감일 기준으로 정렬 (가까운 순)
def get_deadline(program):
if program['deadline']:
try:
return datetime.strptime(program['deadline'], '%Y-%m-%d')
except:
return datetime.max
return datetime.max
return sorted(programs, key=get_deadline)
def get_program_detail(self, program_id: str) -> Optional[Dict]:
"""특정 지원사업 상세 정보 조회"""
# ID에 따라 적절한 소스에서 상세 정보 조회
if program_id.startswith('data_gov_'):
return self._get_data_go_kr_detail(program_id)
elif any(region in program_id for region in ['서울', '경기', '부산', '광주', '대구']):
return self._get_region_detail(program_id)
else:
return None
def _get_data_go_kr_detail(self, program_id: str) -> Optional[Dict]:
"""공공데이터포털 상세 정보 조회"""
return None
def _get_region_detail(self, program_id: str) -> Optional[Dict]:
"""지자체 상세 정보 조회"""
return None
def search_startup_support(region: str = '전국', keyword: Optional[str] = None,
support_type: Optional[str] = None, deadline_only: bool = False) -> List[Dict]:
"""
스타트업 지원사업 검색 함수
Args:
region: 지역 (서울특별시, 경기도, 부산광역시 )
keyword: 검색 키워드
support_type: 지원 유형 (보조금, 융자, 멘토링 )
deadline_only: 마감 임박 사업만 검색
Returns:
지원사업 목록
"""
api = StartupSupportAPI()
return api.search_programs(region, keyword, support_type, deadline_only)
def get_startup_program_detail(program_id: str) -> Optional[Dict]:
"""
특정 지원사업 상세 정보 조회 함수
Args:
program_id: 지원사업 ID
Returns:
지원사업 상세 정보
"""
api = StartupSupportAPI()
return api.get_program_detail(program_id)
if __name__ == "__main__":
# 테스트용 실행
print("스타트업 지원사업 검색 테스트")
# 전체 검색
programs = search_startup_support()
print(f"{len(programs)}개 지원사업 발견")
# 서울 검색
seoul_programs = search_startup_support(region='서울특별시')
print(f"서울 지원사업: {len(seoul_programs)}")
# 키워드 검색
keyword_programs = search_startup_support(keyword='청년')
print(f"'청년' 키워드 검색 결과: {len(keyword_programs)}")
# 마감 임박 검색
deadline_programs = search_startup_support(deadline_only=True)
print(f"마감 임박 지원사업: {len(deadline_programs)}")
raise SystemExit(run())

View file

@ -1,305 +1,149 @@
#!/usr/bin/env python3
from __future__ import annotations
import unittest
import sys
import argparse
import json
import os
from unittest.mock import patch, MagicMock
from datetime import datetime, timedelta
import sys
import unittest
from io import StringIO
from unittest import mock
# 현재 디렉토리에서 모듈 임포트
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from startup_support import search_startup_support, get_startup_program_detail, StartupSupportAPI
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)
class TestStartupSupport(unittest.TestCase):
"""스타트업 지원사업 API 테스트"""
import startup_support # noqa: E402
def setUp(self):
"""테스트 초기화"""
soon_deadline = (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d')
later_deadline = (datetime.now() + timedelta(days=10)).strftime('%Y-%m-%d')
self.test_programs = [
{
'id': 'test_001',
'title': '서울시 청년 스타트업 창업 지원금',
'organization': '서울시',
'region': '서울특별시',
'support_type': '보조금',
'amount': '최대 5천만원',
'deadline': later_deadline,
'target': '만 19~34세 청년 창업가',
'contact': '02-1234-5678',
'url': 'https://seoulstartup.go.kr/program/001',
'source': '서울시 창업플러스',
'last_updated': '2024-05-20'
},
{
'id': 'test_002',
'title': '경기도 MVP 지원사업',
'organization': '경기도',
'region': '경기도',
'support_type': '보조금',
'amount': '최대 3천만원',
'deadline': soon_deadline,
'target': 'MVP 개발 스타트업',
'contact': '031-1234-5678',
'url': 'https://g-startup.kr/program/002',
'source': '경기도 창업진흥원',
'last_updated': '2024-05-20'
}
]
@patch('startup_support.StartupSupportAPI._search_data_go_kr')
@patch('startup_support.StartupSupportAPI._search_by_region')
def test_search_programs_basic(self, mock_region_search, mock_data_go_kr_search):
"""기본 검색 테스트"""
# 모킹 설정
mock_data_go_kr_search.return_value = []
mock_region_search.return_value = self.test_programs
def make_args(**overrides: object) -> argparse.Namespace:
defaults = {
"region": "서울특별시",
"keyword": "청년",
"support_type": None,
"deadline_only": False,
"page": 1,
"per_page": 5,
"text": False,
"dry_run": False,
"timeout": 30,
"proxy_base_url": "https://proxy.example",
}
defaults.update(overrides)
return argparse.Namespace(**defaults)
# 검색 실행
result = search_startup_support()
# 결과 확인
self.assertEqual(len(result), 2)
self.assertEqual(result[0]['title'], '경기도 MVP 지원사업')
self.assertEqual(result[1]['title'], '서울시 청년 스타트업 창업 지원금')
class StartupSupportHelperTests(unittest.TestCase):
def test_build_query_maps_startup_terms_to_kstartup_announcements(self) -> None:
args = make_args(deadline_only=True, support_type="사업화")
@patch('startup_support.requests.get')
def test_nationwide_search_aggregates_configured_regions_without_api_key(self, mock_get):
payloads = {
'https://seoulstartup.go.kr/api/program/list': {
'programs': [{
'id': 'seoul_1',
'title': '서울 창업 지원',
'deadline': '2026-06-03',
}]
},
'https://g-startup.kr/api/support/list': {
'programs': [{
'id': 'gyeonggi_1',
'title': '경기 창업 지원',
'deadline': '2026-06-04',
}]
},
query = startup_support.build_query(args)
self.assertEqual(query["supt_regin"], "서울특별시")
self.assertEqual(query["biz_pbanc_nm"], "청년")
self.assertEqual(query["supt_biz_clsfc"], "사업화")
self.assertEqual(query["rcrt_prgs_yn"], "Y")
self.assertEqual(query["page"], 1)
self.assertEqual(query["perPage"], 5)
self.assertEqual(query["returnType"], "json")
def test_build_query_rejects_bad_page_size(self) -> None:
with self.assertRaises(startup_support.HelperError):
startup_support.build_query(make_args(per_page=0))
with self.assertRaises(startup_support.HelperError):
startup_support.build_query(make_args(per_page=101))
def test_dry_run_uses_hosted_proxy_without_requests_dependency_or_api_key(self) -> None:
out = StringIO()
with mock.patch.object(sys, "stdout", out):
rc = startup_support.run([
"--region", "서울특별시",
"--keyword", "청년",
"--deadline-only",
"--per-page", "5",
"--dry-run",
"--proxy-base-url", "https://proxy.example",
])
self.assertEqual(rc, 0)
payload = json.loads(out.getvalue())
self.assertEqual(payload["operation"], "announcements")
self.assertTrue(payload["url"].startswith("https://proxy.example/v1/kstartup/announcements?"))
self.assertIn("rcrt_prgs_yn=Y", payload["url"])
self.assertNotIn("ServiceKey", payload["url"])
def test_dry_run_uses_env_proxy_base_url_when_cli_option_is_absent(self) -> None:
out = StringIO()
with mock.patch.dict(os.environ, {"KSKILL_PROXY_BASE_URL": "https://env-proxy.example"}):
with mock.patch.object(sys, "stdout", out):
rc = startup_support.run(["--dry-run"])
self.assertEqual(rc, 0)
payload = json.loads(out.getvalue())
self.assertTrue(payload["url"].startswith("https://env-proxy.example/v1/kstartup/announcements?"))
def test_search_helper_uses_env_proxy_base_url_when_argument_is_absent(self) -> None:
payload = {"data": []}
calls = []
def fake_http_get(url: str, *, timeout: int) -> tuple[int, str, str]:
calls.append((url, timeout))
return 200, "application/json", json.dumps(payload)
with mock.patch.dict(os.environ, {"KSKILL_PROXY_BASE_URL": "https://env-proxy.example"}):
with mock.patch.object(startup_support, "http_get", side_effect=fake_http_get):
result = startup_support.search_startup_support()
self.assertEqual(result, [])
self.assertEqual(len(calls), 1)
self.assertTrue(calls[0][0].startswith("https://env-proxy.example/v1/kstartup/announcements?"))
def test_search_startup_support_parses_proxy_payload_and_filters_deadline(self) -> None:
payload = {
"data": [
{
"pbanc_sn": "A1",
"biz_pbanc_nm": "서울 청년 창업 지원",
"sprv_inst": "창업진흥원",
"supt_regin": "서울",
"supt_biz_clsfc": "사업화",
"pbanc_rcpt_bgng_dt": "20260601",
"pbanc_rcpt_end_dt": "20260630",
"aply_trgt": "예비창업자",
"detl_pg_url": "https://www.k-startup.go.kr/detail/A1",
},
{
"pbanc_sn": "B1",
"biz_pbanc_nm": "부산 청년 창업 지원",
"supt_regin": "부산",
"pbanc_rcpt_end_dt": "20260630",
"detl_pg_url": "https://www.k-startup.go.kr/detail/B1",
},
{
"pbanc_sn": "C1",
"biz_pbanc_nm": "전국 창업 지원",
"supt_regin": "전국",
"pbanc_rcpt_end_dt": "20260630",
"detl_pg_url": "https://www.k-startup.go.kr/detail/C1",
}
]
}
def fake_get(url, **_):
response = MagicMock()
response.status_code = 200
response.json.return_value = payloads.get(url, {'programs': []})
return response
with mock.patch.object(startup_support, "http_get", return_value=(200, "application/json", json.dumps(payload))):
result = startup_support.search_startup_support(
region="서울특별시",
keyword="청년",
deadline_only=True,
proxy_base_url="https://proxy.example",
)
mock_get.side_effect = fake_get
with patch.dict(os.environ, {}, clear=True):
result = search_startup_support(region='전국')
titles = {program['title'] for program in result}
self.assertIn('서울 창업 지원', titles)
self.assertIn('경기 창업 지원', titles)
def test_builtin_detail_lookup_does_not_return_fabricated_sample_data(self):
self.assertIsNone(get_startup_program_detail('data_gov_missing'))
self.assertIsNone(get_startup_program_detail('서울_missing'))
@patch('startup_support.StartupSupportAPI._search_data_go_kr')
@patch('startup_support.StartupSupportAPI._search_by_region')
def test_search_programs_seoul_only(self, mock_region_search, mock_data_go_kr_search):
"""서울 지역 검색 테스트"""
# 모킹 설정
mock_data_go_kr_search.return_value = []
mock_region_search.return_value = [self.test_programs[0]] # 서울 프로그램만
# 검색 실행
result = search_startup_support(region='서울특별시')
# 결과 확인
self.assertEqual(len(result), 1)
self.assertEqual(result[0]['region'], '서울특별시')
self.assertEqual(result[0]["id"], "A1")
self.assertEqual(result[0]["title"], "서울 청년 창업 지원")
self.assertEqual(result[0]["deadline"], "2026-06-30")
self.assertEqual(result[0]["url"], "https://www.k-startup.go.kr/detail/A1")
@patch('startup_support.StartupSupportAPI._search_data_go_kr')
@patch('startup_support.StartupSupportAPI._search_by_region')
def test_search_programs_keyword_search(self, mock_region_search, mock_data_go_kr_search):
"""키워드 검색 테스트"""
# 모킹 설정
mock_data_go_kr_search.return_value = []
mock_region_search.return_value = [self.test_programs[1]] # MVP 프로그램만
# 검색 실행
result = search_startup_support(keyword='MVP')
# 결과 확인
self.assertEqual(len(result), 1)
self.assertEqual(result[0]['title'], '경기도 MVP 지원사업')
@patch('startup_support.StartupSupportAPI._search_data_go_kr')
@patch('startup_support.StartupSupportAPI._search_by_region')
def test_search_programs_deadline_only(self, mock_region_search, mock_data_go_kr_search):
"""마감 임박 검색 테스트"""
# 모킹 설정
mock_data_go_kr_search.return_value = []
mock_region_search.return_value = self.test_programs
# 검색 실행
result = search_startup_support(deadline_only=True)
# 결과 확인 (7일 이내 마감만)
self.assertEqual(len(result), 1)
for program in result:
deadline = datetime.strptime(program['deadline'], '%Y-%m-%d')
self.assertTrue(datetime.now() <= deadline <= datetime.now() + timedelta(days=7))
@patch('startup_support.StartupSupportAPI._get_data_go_kr_detail')
def test_get_program_detail_data_gov(self, mock_get_detail):
"""공공데이터포털 상세 정보 조회 테스트"""
# 모킹 설정
mock_get_detail.return_value = self.test_programs[0]
# 상세 정보 조회
result = get_startup_program_detail('data_gov_test_001')
# 결과 확인
self.assertIsNotNone(result)
assert result is not None
self.assertEqual(result['title'], '서울시 청년 스타트업 창업 지원금')
@patch('startup_support.StartupSupportAPI._get_region_detail')
def test_get_program_detail_region(self, mock_get_detail):
"""지자체 상세 정보 조회 테스트"""
# 모킹 설정
mock_get_detail.return_value = self.test_programs[1]
# 상세 정보 조회
result = get_startup_program_detail('서울_test_001')
# 결과 확인
self.assertIsNotNone(result)
assert result is not None
self.assertEqual(result['title'], '경기도 MVP 지원사업')
def test_parse_program_from_data_go_kr(self):
"""공공데이터포털 데이터 파싱 테스트"""
api = StartupSupportAPI()
# 테스트 데이터
item = {
'pan_id': 'test_001',
'pan_nm': '테스트 지원사업',
'cnp_cd_nm': '서울특별시',
'support_type': '보조금',
'amount': '최대 5천만원',
'clsg_dt': '2024-12-31',
'target': '청년 창업가',
'contact': '02-1234-5678',
'detail_url': 'https://test.com',
'last_updated': '2024-05-20'
}
# 파싱 실행
result = api._parse_program_from_data_go_kr(item)
# 결과 확인
self.assertIsNotNone(result)
assert result is not None
self.assertEqual(result['title'], '테스트 지원사업')
self.assertEqual(result['region'], '서울특별시')
self.assertEqual(result['support_type'], '보조금')
def test_parse_program_from_region_api(self):
"""지자체 API 데이터 파싱 테스트"""
from startup_support import StartupSupportAPI
api = StartupSupportAPI()
# 테스트 데이터
item = {
'id': 'test_001',
'title': '테스트 지원사업',
'type': '융자',
'amount': '최대 1억원',
'deadline': '2024-12-31',
'target': '중소기업',
'contact': '02-1234-5678',
'url': 'https://test.com',
'last_updated': '2024-05-20'
}
# 파싱 실행
result = api._parse_program_from_region_api(item, '경기도')
# 결과 확인
self.assertIsNotNone(result)
assert result is not None
self.assertEqual(result['title'], '테스트 지원사업')
self.assertEqual(result['organization'], '경기도 창업진흥원')
self.assertEqual(result['support_type'], '융자')
def test_filter_upcoming_deadline(self):
"""마감 임박 필터링 테스트"""
from startup_support import StartupSupportAPI
from datetime import datetime, timedelta
api = StartupSupportAPI()
# 테스트 데이터 (다양한 마감일)
programs = [
{'deadline': (datetime.now() + timedelta(days=3)).strftime('%Y-%m-%d')}, # 3일 후
{'deadline': (datetime.now() + timedelta(days=10)).strftime('%Y-%m-%d')}, # 10일 후
{'deadline': (datetime.now() - timedelta(days=5)).strftime('%Y-%m-%d')}, # 5일 전
{'deadline': '2024-12-31'}, # 먼 미래
{'deadline': ''} # 마감일 없음
]
# 필터링 실행
result = api._filter_upcoming_deadline(programs)
# 결과 확인 (7일 이내이면서 이미 지난 날짜 제외)
self.assertEqual(len(result), 1)
def test_remove_duplicates(self):
"""중복 제거 테스트"""
from startup_support import StartupSupportAPI
api = StartupSupportAPI()
# 테스트 데이터 (중복 포함)
programs = [
{'id': 'test_001', 'title': '프로그램 A'},
{'id': 'test_002', 'title': '프로그램 B'},
{'id': 'test_001', 'title': '프로그램 A (중복)'},
{'id': 'test_003', 'title': '프로그램 C'}
]
# 중복 제거 실행
result = api._remove_duplicates(programs)
# 결과 확인 (중복 제외)
self.assertEqual(len(result), 3)
self.assertEqual(result[0]['id'], 'test_001')
self.assertEqual(result[1]['id'], 'test_002')
self.assertEqual(result[2]['id'], 'test_003')
def run_tests():
"""테스트 실행"""
# 테스트 스위트 생성
suite = unittest.TestLoader().loadTestsFromTestCase(TestStartupSupport)
# 테스트 실행기 생성
runner = unittest.TextTestRunner(verbosity=2)
# 테스트 실행
result = runner.run(suite)
return result.wasSuccessful()
if __name__ == '__main__':
print("스타트업 지원사업 API 테스트 시작")
# 테스트 실행
success = run_tests()
if success:
print("✅ 모든 테스트 통과!")
else:
print("❌ 일부 테스트 실패")
sys.exit(1)
if __name__ == "__main__":
unittest.main(verbosity=2)