fix(startup-support): remove fabricated detail data

This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-05-31 17:23:57 +09:00
commit cff6b29ff9
7 changed files with 165 additions and 199 deletions

View file

@ -85,7 +85,7 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
| ~~근처 블루리본 맛집~~ ⚠️ 지원 중단 | ~~`blue-ribbon-nearby`~~ | ~~현재 위치 기준 근처 블루리본 선정 맛집 조회~~ | ~~불필요~~ | ~~[근처 블루리본 맛집 가이드](docs/features/blue-ribbon-nearby.md)~~ | | ~~근처 블루리본 맛집~~ ⚠️ 지원 중단 | ~~`blue-ribbon-nearby`~~ | ~~현재 위치 기준 근처 블루리본 선정 맛집 조회~~ | ~~불필요~~ | ~~[근처 블루리본 맛집 가이드](docs/features/blue-ribbon-nearby.md)~~ |
| 근처 술집 조회 | `kakao-bar-nearby` | 현재 위치 기준 영업 상태·메뉴·좌석·전화번호가 포함된 근처 술집 조회 | 불필요 | [근처 술집 조회 가이드](docs/features/kakao-bar-nearby.md) | | 근처 술집 조회 | `kakao-bar-nearby` | 현재 위치 기준 영업 상태·메뉴·좌석·전화번호가 포함된 근처 술집 조회 | 불필요 | [근처 술집 조회 가이드](docs/features/kakao-bar-nearby.md) |
| 우편번호 검색 | `zipcode-search` | 주소 키워드로 우편번호 + 공식 영문주소 조회 | 불필요 | [우편번호 검색 가이드](docs/features/zipcode-search.md) | | 우편번호 검색 | `zipcode-search` | 주소 키워드로 우편번호 + 공식 영문주소 조회 | 불필요 | [우편번호 검색 가이드](docs/features/zipcode-search.md) |
| startup-support | Search Korean government startup support programs, grants, and subsidies for startups, SMEs, and entrepreneurs. Use when users ask about 창업 지원, 스타트업 지원금, 중소기업 지원, 정부 지원사업. | No login | [docs/features/startup-support.md](https://github.com/k-skill/k-skill/blob/main/docs/features/startup-support.md) | | 창업 지원사업 조회 | `startup-support` | 정부·지자체 스타트업 지원사업 목록 조회 (상세 조회는 원본 공고 링크로 확인) | 불필요 | [창업 지원사업 조회 가이드](docs/features/startup-support.md) |
| 다이소 상품 조회 | `daiso-product-search` | 다이소 매장별 상품 픽업 가능 여부 확인 (정확한 매장별 재고 수량은 다이소몰 보안 정책으로 2026-05-05 부터 차단됨) | 불필요 | [다이소 상품 조회 가이드](docs/features/daiso-product-search.md) | | 다이소 상품 조회 | `daiso-product-search` | 다이소 매장별 상품 픽업 가능 여부 확인 (정확한 매장별 재고 수량은 다이소몰 보안 정책으로 2026-05-05 부터 차단됨) | 불필요 | [다이소 상품 조회 가이드](docs/features/daiso-product-search.md) |
| 강남언니 병원 조회 | `gangnamunni-clinic-search` | 강남언니 공개 검색 페이지에서 성형외과·피부과 병원 후보, 평점, 리뷰 수, 지원 언어, 공개 링크 조회 | 불필요 | [강남언니 병원 조회 가이드](docs/features/gangnamunni-clinic-search.md) | | 강남언니 병원 조회 | `gangnamunni-clinic-search` | 강남언니 공개 검색 페이지에서 성형외과·피부과 병원 후보, 평점, 리뷰 수, 지원 언어, 공개 링크 조회 | 불필요 | [강남언니 병원 조회 가이드](docs/features/gangnamunni-clinic-search.md) |
| 마켓컬리 상품 조회 | `market-kurly-search` | 마켓컬리 상품 검색, 현재 가격, 할인 여부, 품절 여부 조회 | 불필요 | [마켓컬리 상품 조회 가이드](docs/features/market-kurly-search.md) | | 마켓컬리 상품 조회 | `market-kurly-search` | 마켓컬리 상품 검색, 현재 가격, 할인 여부, 품절 여부 조회 | 불필요 | [마켓컬리 상품 조회 가이드](docs/features/market-kurly-search.md) |

View file

@ -43,10 +43,7 @@
#### k-skill-proxy 라우트 #### k-skill-proxy 라우트
- `GET /v1/startup-support/list`: 지원사업 목록 조회 공공데이터포털 K-Startup OpenAPI는 별도 `kstartup-search` 스킬과 `k-skill-proxy``/v1/kstartup/*` 라우트가 담당합니다. `startup-support` helper는 지역별 공개 API 목록을 조회하고, 상세 정보는 결과의 공식 `url` 로 확인합니다.
- `GET /v1/startup-support/detail/:program_id`: 특정 지원사업 상세 정보
- `GET /v1/startup-support/region/:region`: 특정 지역 지원사업 조회
- `GET /v1/startup-support/deadline`: 임박 마감 지원사업
#### Python 스크립트 #### Python 스크립트
@ -63,8 +60,7 @@ keyword_programs = search_startup_support(keyword='청년')
# 마감 임박 검색 # 마감 임박 검색
deadline_programs = search_startup_support(deadline_only=True) deadline_programs = search_startup_support(deadline_only=True)
# 상세 정보 조회 # 상세 정보는 목록 결과의 공식 url로 확인
detail = get_startup_program_detail('test_001')
``` ```
## 데이터 소스 ## 데이터 소스
@ -72,7 +68,7 @@ detail = get_startup_program_detail('test_001')
### 1. 공공데이터포털 ### 1. 공공데이터포털
- **기관**: 중소벤처기업부 - **기관**: 중소벤처기업부
- **API**: 스타트업 지원사업 정보 - **API**: 스타트업 지원사업 정보
- **인증**: API 키 필요 (DATA_GO_KR_API_KEY) - **인증**: hosted/self-host proxy 운영 서버에서 API 키 주입
### 2. 지자체별 사이트 ### 2. 지자체별 사이트
- **서울시**: https://seoulstartup.go.kr - **서울시**: https://seoulstartup.go.kr

View file

@ -428,9 +428,7 @@ node scripts/korean_character_count.js --text $'첫 줄\n둘째 줄🙂' --profi
startup-support 스킬은 다음과 같은 환경이 필요합니다: startup-support 스킬은 다음과 같은 환경이 필요합니다:
#### 환경 변수 #### 환경 변수
```bash 기본 사용자는 별도 API 키가 필요 없습니다. `DATA_GO_KR_API_KEY` 는 hosted/self-host `k-skill-proxy` 운영자가 서버에 설정하는 값입니다.
export DATA_GO_KR_API_KEY="your_api_key_here"
```
#### 의존성 #### 의존성
- Python 3.7+ - Python 3.7+

View file

@ -89,10 +89,9 @@ KSKILL_PROXY_BASE_URL=
### startup-support ### startup-support
#### API 키 관리 #### API 키 관리
- **환경 변수**: `DATA_GO_KR_API_KEY` - 기본 사용자는 `DATA_GO_KR_API_KEY` 를 설정하지 않습니다.
- **용도**: 공공데이터포털 API 인증 - `DATA_GO_KR_API_KEY` 는 hosted/self-host `k-skill-proxy` 운영자가 서버에 설정하는 upstream 키입니다.
- **보안**: 절대 코드에 하드코딩하지 않음 - 로컬에서 직접 공공데이터포털 API를 호출하는 실험 경로에서만 사용자 환경에 임시로 둘 수 있습니다.
- **관리**: 환경 변수 또는 시크릿 매니저를 통해 관리
#### 데이터 보안 #### 데이터 보안
- **데이터 소스**: 공공기관 공식 API만 사용 - **데이터 소스**: 공공기관 공식 API만 사용

View file

@ -65,12 +65,7 @@ metadata:
### Proxy Integration ### Proxy Integration
API 요청은 `k-skill-proxy``/v1/startup-support/*` 라우트로 중계되며, 다음과 같은 엔드포인트를 사용: 공공데이터포털 K-Startup OpenAPI는 `kstartup-search` 스킬과 `k-skill-proxy``/v1/kstartup/*` 라우트가 담당한다. 이 스킬의 helper는 지역별 공개 API 목록을 조회하고, 상세 정보는 결과의 공식 `url` 로 확인한다.
- `/v1/startup-support/list` - 지원사업 목록 조회
- `/v1/startup-support/detail/<program_id>` - 특정 지원사업 상세 정보
- `/v1/startup-support/region/<region>` - 특정 지역 지원사업 조회
- `/v1/startup-support/deadline` - 임박 마감 지원사업
## Output format ## Output format

View file

@ -9,7 +9,7 @@ import os
class StartupSupportAPI: class StartupSupportAPI:
"""스타트업 지원사업 API 클라이언트""" """스타트업 지원사업 API 클라이언트"""
def __init__(self): def __init__(self):
self.base_urls = { self.base_urls = {
'seoul': 'https://seoulstartup.go.kr', 'seoul': 'https://seoulstartup.go.kr',
@ -19,96 +19,96 @@ class StartupSupportAPI:
'daegu': 'https://daegu-startup.kr', 'daegu': 'https://daegu-startup.kr',
'nationwide': 'https://www.data.go.kr' 'nationwide': 'https://www.data.go.kr'
} }
# 공공데이터포털 API 키 (환경 변수에서 가져오기) # 공공데이터포털 API 키 (환경 변수에서 가져오기)
self.data_go_kr_api_key = os.getenv('DATA_GO_KR_API_KEY') self.data_go_kr_api_key = os.getenv('DATA_GO_KR_API_KEY')
# 헤더 설정 # 헤더 설정
self.headers = { 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', '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', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
def search_programs(self, region: str = '전국', keyword: str = None, def search_programs(self, region: str = '전국', keyword: Optional[str] = None,
support_type: str = None, deadline_only: bool = False) -> List[Dict]: support_type: Optional[str] = None, deadline_only: bool = False) -> List[Dict]:
""" """
지원사업 검색 지원사업 검색
Args: Args:
region: 지역 (서울특별시, 경기도, 부산광역시 ) region: 지역 (서울특별시, 경기도, 부산광역시 )
keyword: 검색 키워드 keyword: 검색 키워드
support_type: 지원 유형 (보조금, 융자, 멘토링 ) support_type: 지원 유형 (보조금, 융자, 멘토링 )
deadline_only: 마감 임박 사업만 검색 deadline_only: 마감 임박 사업만 검색
Returns: Returns:
지원사업 목록 지원사업 목록
""" """
programs = [] programs = []
# 1. 공공데이터포털 API 호출 # 1. 공공데이터포털 API 호출
if self.data_go_kr_api_key: if self.data_go_kr_api_key:
data_go_kr_programs = self._search_data_go_kr(region, keyword, support_type) data_go_kr_programs = self._search_data_go_kr(region, keyword, support_type)
programs.extend(data_go_kr_programs) programs.extend(data_go_kr_programs)
# 2. 지자체별 API 호출 # 2. 지자체별 API 호출
region_programs = self._search_by_region(region, keyword, support_type) region_programs = self._search_by_region(region, keyword, support_type)
programs.extend(region_programs) programs.extend(region_programs)
# 3. 마감 임박 필터링 # 3. 마감 임박 필터링
if deadline_only: if deadline_only:
programs = self._filter_upcoming_deadline(programs) programs = self._filter_upcoming_deadline(programs)
# 중복 제거 # 중복 제거
programs = self._remove_duplicates(programs) programs = self._remove_duplicates(programs)
# 정렬 # 정렬
programs = self._sort_programs(programs) programs = self._sort_programs(programs)
return programs return programs
def _search_data_go_kr(self, region: str, keyword: str, support_type: str) -> List[Dict]: def _search_data_go_kr(self, region: str, keyword: Optional[str], support_type: Optional[str]) -> List[Dict]:
"""공공데이터포털 API로 검색""" """공공데이터포털 API로 검색"""
programs = [] programs = []
try: try:
# 중소벤처기업부 스타트업 지원사업 API # 중소벤처기업부 스타트업 지원사업 API
url = "https://www.data.go.kr/api/15058530/openapi" url = "https://www.data.go.kr/api/15058530/openapi"
params = { params = {
'serviceKey': self.data_go_kr_api_key, 'serviceKey': self.data_go_kr_api_key,
'pageNo': '1', 'pageNo': '1',
'numOfRows': '100', 'numOfRows': '100',
'_type': 'json' '_type': 'json'
} }
if region and region != '전국': if region and region != '전국':
params['cnpCdNm'] = region params['cnpCdNm'] = region
if keyword: if keyword:
params['panNm'] = keyword params['panNm'] = keyword
response = requests.get(url, params=params, headers=self.headers, timeout=10) response = requests.get(url, params=params, headers=self.headers, timeout=10)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
# 실제 API 응답 구조에 따라 데이터 추출 # 실제 API 응답 구조에 따라 데이터 추출
if 'items' in data: if 'items' in data:
for item in data['items']: for item in data['items']:
program = self._parse_program_from_data_go_kr(item) program = self._parse_program_from_data_go_kr(item)
if program: if program:
programs.append(program) programs.append(program)
except Exception as e: except Exception as e:
print(f"공공데이터포털 API 오류: {e}") print(f"공공데이터포털 API 오류: {e}")
return programs return programs
def _search_by_region(self, region: str, keyword: str, support_type: str) -> List[Dict]: def _search_by_region(self, region: str, keyword: Optional[str], support_type: Optional[str]) -> List[Dict]:
"""지자체별 API로 검색""" """지자체별 API로 검색"""
programs = [] programs = []
# 지자체별 API 엔드포인트 # 지자체별 API 엔드포인트
region_apis = { region_apis = {
'서울특별시': { '서울특별시': {
@ -132,36 +132,38 @@ class StartupSupportAPI:
'method': 'GET' 'method': 'GET'
} }
} }
# 해당 지역 API 호출 target_regions = list(region_apis) if region == '전국' else [region]
if region in region_apis: for target_region in target_regions:
api_info = region_apis[region] if target_region not in region_apis:
continue
api_info = region_apis[target_region]
try: try:
params = {} params = {}
if keyword: if keyword:
params['keyword'] = keyword params['keyword'] = keyword
if support_type: if support_type:
params['type'] = support_type params['type'] = support_type
response = requests.get(api_info['url'], params=params, response = requests.get(api_info['url'], params=params,
headers=self.headers, timeout=10) headers=self.headers, timeout=10)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
# 실제 API 응답 구조에 따라 데이터 추출 # 실제 API 응답 구조에 따라 데이터 추출
if 'programs' in data: if 'programs' in data:
for item in data['programs']: for item in data['programs']:
program = self._parse_program_from_region_api(item, region) program = self._parse_program_from_region_api(item, target_region)
if program: if program:
programs.append(program) programs.append(program)
except Exception as e: except Exception as e:
print(f"{region} API 오류: {e}") print(f"{target_region} API 오류: {e}")
return programs return programs
def _parse_program_from_data_go_kr(self, item: Dict) -> Optional[Dict]: def _parse_program_from_data_go_kr(self, item: Dict) -> Optional[Dict]:
"""공공데이터포털 응답 파싱""" """공공데이터포털 응답 파싱"""
try: try:
@ -179,17 +181,17 @@ class StartupSupportAPI:
'source': '공공데이터포털', 'source': '공공데이터포털',
'last_updated': item.get('last_updated', datetime.now().strftime('%Y-%m-%d')) 'last_updated': item.get('last_updated', datetime.now().strftime('%Y-%m-%d'))
} }
# 필수 필드 검증 # 필수 필드 검증
if not program['title']: if not program['title']:
return None return None
return program return program
except Exception as e: except Exception as e:
print(f"공공데이터포털 데이터 파싱 오류: {e}") print(f"공공데이터포털 데이터 파싱 오류: {e}")
return None return None
def _parse_program_from_region_api(self, item: Dict, region: str) -> Optional[Dict]: def _parse_program_from_region_api(self, item: Dict, region: str) -> Optional[Dict]:
"""지자체 API 응답 파싱""" """지자체 API 응답 파싱"""
try: try:
@ -207,24 +209,24 @@ class StartupSupportAPI:
'source': region + ' 창업진흥원', 'source': region + ' 창업진흥원',
'last_updated': item.get('last_updated', datetime.now().strftime('%Y-%m-%d')) 'last_updated': item.get('last_updated', datetime.now().strftime('%Y-%m-%d'))
} }
# 필수 필드 검증 # 필수 필드 검증
if not program['title']: if not program['title']:
return None return None
return program return program
except Exception as e: except Exception as e:
print(f"지자체 API 데이터 파싱 오류: {e}") print(f"지자체 API 데이터 파싱 오류: {e}")
return None return None
def _filter_upcoming_deadline(self, programs: List[Dict]) -> List[Dict]: def _filter_upcoming_deadline(self, programs: List[Dict]) -> List[Dict]:
"""마감 임박 사업 필터링""" """마감 임박 사업 필터링"""
today = datetime.now() today = datetime.now()
upcoming_threshold = today + timedelta(days=7) # 7일 이내 upcoming_threshold = today + timedelta(days=7) # 7일 이내
filtered = [] filtered = []
for program in programs: for program in programs:
if program['deadline']: if program['deadline']:
try: try:
@ -234,22 +236,22 @@ class StartupSupportAPI:
except: except:
# 날짜 파싱 실패 시 제외 # 날짜 파싱 실패 시 제외
continue continue
return filtered return filtered
def _remove_duplicates(self, programs: List[Dict]) -> List[Dict]: def _remove_duplicates(self, programs: List[Dict]) -> List[Dict]:
"""중복 제거""" """중복 제거"""
seen_ids = set() seen_ids = set()
unique_programs = [] unique_programs = []
for program in programs: for program in programs:
program_id = program['id'] program_id = program['id']
if program_id not in seen_ids: if program_id not in seen_ids:
seen_ids.add(program_id) seen_ids.add(program_id)
unique_programs.append(program) unique_programs.append(program)
return unique_programs return unique_programs
def _sort_programs(self, programs: List[Dict]) -> List[Dict]: def _sort_programs(self, programs: List[Dict]) -> List[Dict]:
"""사업 정렬""" """사업 정렬"""
# 마감일 기준으로 정렬 (가까운 순) # 마감일 기준으로 정렬 (가까운 순)
@ -260,9 +262,9 @@ class StartupSupportAPI:
except: except:
return datetime.max return datetime.max
return datetime.max return datetime.max
return sorted(programs, key=get_deadline) return sorted(programs, key=get_deadline)
def get_program_detail(self, program_id: str) -> Optional[Dict]: def get_program_detail(self, program_id: str) -> Optional[Dict]:
"""특정 지원사업 상세 정보 조회""" """특정 지원사업 상세 정보 조회"""
# ID에 따라 적절한 소스에서 상세 정보 조회 # ID에 따라 적절한 소스에서 상세 정보 조회
@ -272,86 +274,26 @@ class StartupSupportAPI:
return self._get_region_detail(program_id) return self._get_region_detail(program_id)
else: else:
return None return None
def _get_data_go_kr_detail(self, program_id: str) -> Optional[Dict]: def _get_data_go_kr_detail(self, program_id: str) -> Optional[Dict]:
"""공공데이터포털 상세 정보 조회""" """공공데이터포털 상세 정보 조회"""
# 실 구현에서는 program_id를 사용해 상세 API 호출 return None
return {
'id': program_id,
'title': '상세 정보 조회 예시',
'organization': '중소벤처기업부',
'region': '전국',
'support_type': '보조금',
'amount': '최대 1억원',
'deadline': '2024-12-31',
'target': '중소기업 창업자',
'requirements': [
'사업자등록증',
'사업계획서',
'재무제표',
'창업자 신분증'
],
'application_process': [
'온라인 신청서 작성',
'서류 제출',
'서류 심사',
'현장 면접',
'결공고'
],
'contact': {
'phone': '02-1234-5678',
'email': 'support@smbs.or.kr',
'address': '서울시 강남구 테헤란로 123'
},
'url': 'https://www.data.go.kr/program/detail',
'source': '공공데이터포털',
'last_updated': datetime.now().strftime('%Y-%m-%d')
}
def _get_region_detail(self, program_id: str) -> Optional[Dict]: def _get_region_detail(self, program_id: str) -> Optional[Dict]:
"""지자체 상세 정보 조회""" """지자체 상세 정보 조회"""
# 실 구현에서는 program_id를 사용해 상세 API 호출 return None
return {
'id': program_id,
'title': '지자체 상세 정보 조회 예시',
'organization': '서울시 창업진흥원',
'region': '서울특별시',
'support_type': '보조금',
'amount': '최대 5천만원',
'deadline': '2024-12-31',
'target': '서울시 내 스타트업',
'requirements': [
'사업자등록증',
'사업계획서',
'재무제표'
],
'application_process': [
'온라인 신청서 작성',
'서류 제출',
'서류 심사',
'결공고'
],
'contact': {
'phone': '02-1234-5678',
'email': 'startup@seoul.go.kr',
'address': '서울시 강남구 테헤란로 123'
},
'url': 'https://seoulstartup.go.kr/program/detail',
'source': '서울시 창업진흥원',
'last_updated': datetime.now().strftime('%Y-%m-%d')
}
def search_startup_support(region: str = '전국', keyword: str = None, def search_startup_support(region: str = '전국', keyword: Optional[str] = None,
support_type: str = None, deadline_only: bool = False) -> List[Dict]: support_type: Optional[str] = None, deadline_only: bool = False) -> List[Dict]:
""" """
스타트업 지원사업 검색 함수 스타트업 지원사업 검색 함수
Args: Args:
region: 지역 (서울특별시, 경기도, 부산광역시 ) region: 지역 (서울특별시, 경기도, 부산광역시 )
keyword: 검색 키워드 keyword: 검색 키워드
support_type: 지원 유형 (보조금, 융자, 멘토링 ) support_type: 지원 유형 (보조금, 융자, 멘토링 )
deadline_only: 마감 임박 사업만 검색 deadline_only: 마감 임박 사업만 검색
Returns: Returns:
지원사업 목록 지원사업 목록
""" """
@ -361,10 +303,10 @@ def search_startup_support(region: str = '전국', keyword: str = None,
def get_startup_program_detail(program_id: str) -> Optional[Dict]: def get_startup_program_detail(program_id: str) -> Optional[Dict]:
""" """
특정 지원사업 상세 정보 조회 함수 특정 지원사업 상세 정보 조회 함수
Args: Args:
program_id: 지원사업 ID program_id: 지원사업 ID
Returns: Returns:
지원사업 상세 정보 지원사업 상세 정보
""" """
@ -374,19 +316,19 @@ def get_startup_program_detail(program_id: str) -> Optional[Dict]:
if __name__ == "__main__": if __name__ == "__main__":
# 테스트용 실행 # 테스트용 실행
print("스타트업 지원사업 검색 테스트") print("스타트업 지원사업 검색 테스트")
# 전체 검색 # 전체 검색
programs = search_startup_support() programs = search_startup_support()
print(f"{len(programs)}개 지원사업 발견") print(f"{len(programs)}개 지원사업 발견")
# 서울 검색 # 서울 검색
seoul_programs = search_startup_support(region='서울특별시') seoul_programs = search_startup_support(region='서울특별시')
print(f"서울 지원사업: {len(seoul_programs)}") print(f"서울 지원사업: {len(seoul_programs)}")
# 키워드 검색 # 키워드 검색
keyword_programs = search_startup_support(keyword='청년') keyword_programs = search_startup_support(keyword='청년')
print(f"'청년' 키워드 검색 결과: {len(keyword_programs)}") print(f"'청년' 키워드 검색 결과: {len(keyword_programs)}")
# 마감 임박 검색 # 마감 임박 검색
deadline_programs = search_startup_support(deadline_only=True) deadline_programs = search_startup_support(deadline_only=True)
print(f"마감 임박 지원사업: {len(deadline_programs)}") print(f"마감 임박 지원사업: {len(deadline_programs)}")

View file

@ -8,11 +8,11 @@ from datetime import datetime, timedelta
# 현재 디렉토리에서 모듈 임포트 # 현재 디렉토리에서 모듈 임포트
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from startup_support import search_startup_support, get_startup_program_detail from startup_support import search_startup_support, get_startup_program_detail, StartupSupportAPI
class TestStartupSupport(unittest.TestCase): class TestStartupSupport(unittest.TestCase):
"""스타트업 지원사업 API 테스트""" """스타트업 지원사업 API 테스트"""
def setUp(self): def setUp(self):
"""테스트 초기화""" """테스트 초기화"""
soon_deadline = (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d') soon_deadline = (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d')
@ -47,7 +47,7 @@ class TestStartupSupport(unittest.TestCase):
'last_updated': '2024-05-20' 'last_updated': '2024-05-20'
} }
] ]
@patch('startup_support.StartupSupportAPI._search_data_go_kr') @patch('startup_support.StartupSupportAPI._search_data_go_kr')
@patch('startup_support.StartupSupportAPI._search_by_region') @patch('startup_support.StartupSupportAPI._search_by_region')
def test_search_programs_basic(self, mock_region_search, mock_data_go_kr_search): def test_search_programs_basic(self, mock_region_search, mock_data_go_kr_search):
@ -55,15 +55,53 @@ class TestStartupSupport(unittest.TestCase):
# 모킹 설정 # 모킹 설정
mock_data_go_kr_search.return_value = [] mock_data_go_kr_search.return_value = []
mock_region_search.return_value = self.test_programs mock_region_search.return_value = self.test_programs
# 검색 실행 # 검색 실행
result = search_startup_support() result = search_startup_support()
# 결과 확인 # 결과 확인
self.assertEqual(len(result), 2) self.assertEqual(len(result), 2)
self.assertEqual(result[0]['title'], '경기도 MVP 지원사업') self.assertEqual(result[0]['title'], '경기도 MVP 지원사업')
self.assertEqual(result[1]['title'], '서울시 청년 스타트업 창업 지원금') self.assertEqual(result[1]['title'], '서울시 청년 스타트업 창업 지원금')
@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',
}]
},
}
def fake_get(url, **_):
response = MagicMock()
response.status_code = 200
response.json.return_value = payloads.get(url, {'programs': []})
return response
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_data_go_kr')
@patch('startup_support.StartupSupportAPI._search_by_region') @patch('startup_support.StartupSupportAPI._search_by_region')
def test_search_programs_seoul_only(self, mock_region_search, mock_data_go_kr_search): def test_search_programs_seoul_only(self, mock_region_search, mock_data_go_kr_search):
@ -71,14 +109,14 @@ class TestStartupSupport(unittest.TestCase):
# 모킹 설정 # 모킹 설정
mock_data_go_kr_search.return_value = [] mock_data_go_kr_search.return_value = []
mock_region_search.return_value = [self.test_programs[0]] # 서울 프로그램만 mock_region_search.return_value = [self.test_programs[0]] # 서울 프로그램만
# 검색 실행 # 검색 실행
result = search_startup_support(region='서울특별시') result = search_startup_support(region='서울특별시')
# 결과 확인 # 결과 확인
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assertEqual(result[0]['region'], '서울특별시') self.assertEqual(result[0]['region'], '서울특별시')
@patch('startup_support.StartupSupportAPI._search_data_go_kr') @patch('startup_support.StartupSupportAPI._search_data_go_kr')
@patch('startup_support.StartupSupportAPI._search_by_region') @patch('startup_support.StartupSupportAPI._search_by_region')
def test_search_programs_keyword_search(self, mock_region_search, mock_data_go_kr_search): def test_search_programs_keyword_search(self, mock_region_search, mock_data_go_kr_search):
@ -86,14 +124,14 @@ class TestStartupSupport(unittest.TestCase):
# 모킹 설정 # 모킹 설정
mock_data_go_kr_search.return_value = [] mock_data_go_kr_search.return_value = []
mock_region_search.return_value = [self.test_programs[1]] # MVP 프로그램만 mock_region_search.return_value = [self.test_programs[1]] # MVP 프로그램만
# 검색 실행 # 검색 실행
result = search_startup_support(keyword='MVP') result = search_startup_support(keyword='MVP')
# 결과 확인 # 결과 확인
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assertEqual(result[0]['title'], '경기도 MVP 지원사업') self.assertEqual(result[0]['title'], '경기도 MVP 지원사업')
@patch('startup_support.StartupSupportAPI._search_data_go_kr') @patch('startup_support.StartupSupportAPI._search_data_go_kr')
@patch('startup_support.StartupSupportAPI._search_by_region') @patch('startup_support.StartupSupportAPI._search_by_region')
def test_search_programs_deadline_only(self, mock_region_search, mock_data_go_kr_search): def test_search_programs_deadline_only(self, mock_region_search, mock_data_go_kr_search):
@ -101,7 +139,7 @@ class TestStartupSupport(unittest.TestCase):
# 모킹 설정 # 모킹 설정
mock_data_go_kr_search.return_value = [] mock_data_go_kr_search.return_value = []
mock_region_search.return_value = self.test_programs mock_region_search.return_value = self.test_programs
# 검색 실행 # 검색 실행
result = search_startup_support(deadline_only=True) result = search_startup_support(deadline_only=True)
@ -110,39 +148,37 @@ class TestStartupSupport(unittest.TestCase):
for program in result: for program in result:
deadline = datetime.strptime(program['deadline'], '%Y-%m-%d') deadline = datetime.strptime(program['deadline'], '%Y-%m-%d')
self.assertTrue(datetime.now() <= deadline <= datetime.now() + timedelta(days=7)) self.assertTrue(datetime.now() <= deadline <= datetime.now() + timedelta(days=7))
@patch('startup_support.StartupSupportAPI._get_data_go_kr_detail') @patch('startup_support.StartupSupportAPI._get_data_go_kr_detail')
def test_get_program_detail_data_gov(self, mock_get_detail): def test_get_program_detail_data_gov(self, mock_get_detail):
"""공공데이터포털 상세 정보 조회 테스트""" """공공데이터포털 상세 정보 조회 테스트"""
# 모킹 설정 # 모킹 설정
mock_get_detail.return_value = self.test_programs[0] mock_get_detail.return_value = self.test_programs[0]
# 상세 정보 조회 # 상세 정보 조회
result = get_startup_program_detail('data_gov_test_001') result = get_startup_program_detail('data_gov_test_001')
# 결과 확인 # 결과 확인
self.assertIsNotNone(result) self.assertIsNotNone(result)
self.assertEqual(result['title'], '서울시 청년 스타트업 창업 지원금') self.assertEqual(result['title'], '서울시 청년 스타트업 창업 지원금')
@patch('startup_support.StartupSupportAPI._get_region_detail') @patch('startup_support.StartupSupportAPI._get_region_detail')
def test_get_program_detail_region(self, mock_get_detail): def test_get_program_detail_region(self, mock_get_detail):
"""지자체 상세 정보 조회 테스트""" """지자체 상세 정보 조회 테스트"""
# 모킹 설정 # 모킹 설정
mock_get_detail.return_value = self.test_programs[1] mock_get_detail.return_value = self.test_programs[1]
# 상세 정보 조회 # 상세 정보 조회
result = get_startup_program_detail('서울_test_001') result = get_startup_program_detail('서울_test_001')
# 결과 확인 # 결과 확인
self.assertIsNotNone(result) self.assertIsNotNone(result)
self.assertEqual(result['title'], '경기도 MVP 지원사업') self.assertEqual(result['title'], '경기도 MVP 지원사업')
def test_parse_program_from_data_go_kr(self): def test_parse_program_from_data_go_kr(self):
"""공공데이터포털 데이터 파싱 테스트""" """공공데이터포털 데이터 파싱 테스트"""
from startup_support import StartupSupportAPI
api = StartupSupportAPI() api = StartupSupportAPI()
# 테스트 데이터 # 테스트 데이터
item = { item = {
'pan_id': 'test_001', 'pan_id': 'test_001',
@ -156,22 +192,22 @@ class TestStartupSupport(unittest.TestCase):
'detail_url': 'https://test.com', 'detail_url': 'https://test.com',
'last_updated': '2024-05-20' 'last_updated': '2024-05-20'
} }
# 파싱 실행 # 파싱 실행
result = api._parse_program_from_data_go_kr(item) result = api._parse_program_from_data_go_kr(item)
# 결과 확인 # 결과 확인
self.assertIsNotNone(result) self.assertIsNotNone(result)
self.assertEqual(result['title'], '테스트 지원사업') self.assertEqual(result['title'], '테스트 지원사업')
self.assertEqual(result['region'], '서울특별시') self.assertEqual(result['region'], '서울특별시')
self.assertEqual(result['support_type'], '보조금') self.assertEqual(result['support_type'], '보조금')
def test_parse_program_from_region_api(self): def test_parse_program_from_region_api(self):
"""지자체 API 데이터 파싱 테스트""" """지자체 API 데이터 파싱 테스트"""
from startup_support import StartupSupportAPI from startup_support import StartupSupportAPI
api = StartupSupportAPI() api = StartupSupportAPI()
# 테스트 데이터 # 테스트 데이터
item = { item = {
'id': 'test_001', 'id': 'test_001',
@ -184,23 +220,23 @@ class TestStartupSupport(unittest.TestCase):
'url': 'https://test.com', 'url': 'https://test.com',
'last_updated': '2024-05-20' 'last_updated': '2024-05-20'
} }
# 파싱 실행 # 파싱 실행
result = api._parse_program_from_region_api(item, '경기도') result = api._parse_program_from_region_api(item, '경기도')
# 결과 확인 # 결과 확인
self.assertIsNotNone(result) self.assertIsNotNone(result)
self.assertEqual(result['title'], '테스트 지원사업') self.assertEqual(result['title'], '테스트 지원사업')
self.assertEqual(result['organization'], '경기도 창업진흥원') self.assertEqual(result['organization'], '경기도 창업진흥원')
self.assertEqual(result['support_type'], '융자') self.assertEqual(result['support_type'], '융자')
def test_filter_upcoming_deadline(self): def test_filter_upcoming_deadline(self):
"""마감 임박 필터링 테스트""" """마감 임박 필터링 테스트"""
from startup_support import StartupSupportAPI from startup_support import StartupSupportAPI
from datetime import datetime, timedelta from datetime import datetime, timedelta
api = StartupSupportAPI() api = StartupSupportAPI()
# 테스트 데이터 (다양한 마감일) # 테스트 데이터 (다양한 마감일)
programs = [ programs = [
{'deadline': (datetime.now() + timedelta(days=3)).strftime('%Y-%m-%d')}, # 3일 후 {'deadline': (datetime.now() + timedelta(days=3)).strftime('%Y-%m-%d')}, # 3일 후
@ -209,19 +245,19 @@ class TestStartupSupport(unittest.TestCase):
{'deadline': '2024-12-31'}, # 먼 미래 {'deadline': '2024-12-31'}, # 먼 미래
{'deadline': ''} # 마감일 없음 {'deadline': ''} # 마감일 없음
] ]
# 필터링 실행 # 필터링 실행
result = api._filter_upcoming_deadline(programs) result = api._filter_upcoming_deadline(programs)
# 결과 확인 (7일 이내이면서 이미 지난 날짜 제외) # 결과 확인 (7일 이내이면서 이미 지난 날짜 제외)
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
def test_remove_duplicates(self): def test_remove_duplicates(self):
"""중복 제거 테스트""" """중복 제거 테스트"""
from startup_support import StartupSupportAPI from startup_support import StartupSupportAPI
api = StartupSupportAPI() api = StartupSupportAPI()
# 테스트 데이터 (중복 포함) # 테스트 데이터 (중복 포함)
programs = [ programs = [
{'id': 'test_001', 'title': '프로그램 A'}, {'id': 'test_001', 'title': '프로그램 A'},
@ -229,10 +265,10 @@ class TestStartupSupport(unittest.TestCase):
{'id': 'test_001', 'title': '프로그램 A (중복)'}, {'id': 'test_001', 'title': '프로그램 A (중복)'},
{'id': 'test_003', 'title': '프로그램 C'} {'id': 'test_003', 'title': '프로그램 C'}
] ]
# 중복 제거 실행 # 중복 제거 실행
result = api._remove_duplicates(programs) result = api._remove_duplicates(programs)
# 결과 확인 (중복 제외) # 결과 확인 (중복 제외)
self.assertEqual(len(result), 3) self.assertEqual(len(result), 3)
self.assertEqual(result[0]['id'], 'test_001') self.assertEqual(result[0]['id'], 'test_001')
@ -243,21 +279,21 @@ def run_tests():
"""테스트 실행""" """테스트 실행"""
# 테스트 스위트 생성 # 테스트 스위트 생성
suite = unittest.TestLoader().loadTestsFromTestCase(TestStartupSupport) suite = unittest.TestLoader().loadTestsFromTestCase(TestStartupSupport)
# 테스트 실행기 생성 # 테스트 실행기 생성
runner = unittest.TextTestRunner(verbosity=2) runner = unittest.TextTestRunner(verbosity=2)
# 테스트 실행 # 테스트 실행
result = runner.run(suite) result = runner.run(suite)
return result.wasSuccessful() return result.wasSuccessful()
if __name__ == '__main__': if __name__ == '__main__':
print("스타트업 지원사업 API 테스트 시작") print("스타트업 지원사업 API 테스트 시작")
# 테스트 실행 # 테스트 실행
success = run_tests() success = run_tests()
if success: if success:
print("✅ 모든 테스트 통과!") print("✅ 모든 테스트 통과!")
else: else: