# PRD: 나라장터 입찰공고 자동 큐레이션 봇

**프로젝트명**: G2B Bid Scout (가칭)
**작성일**: 2026-05-17
**작성자**: Teddy (WOWD.LAB)
**버전**: v0.2

---

## 1. 왜 만드는가 (Why)

### 1.1 문제
- 기업교육 강사는 매주 나라장터에 직접 접속해 입찰공고를 수동으로 확인해야 한다
- 용역 카테고리 안에서도 본인 전문 분야(디자인씽킹·AI교육·리빙랩)에 맞는 공고는 소수
- 마감일이 임박한 공고를 놓치면 제안서 준비 시간이 부족해 기회 손실 발생
- 매번 반복되는 작업을 자동화하지 못해 시간 낭비

### 1.2 목표
강사가 매주 월요일 오후 5시에 **이메일 한 통만 확인**하면 다음을 알 수 있도록 한다:
1. 오늘 시점 기준 입찰 진행 중인 적합 공고가 무엇인지
2. 어떤 공고가 마감 임박인지
3. 왜 이 공고가 본인에게 적합한지 (AI 한 줄 평)
4. 원문은 한 번의 클릭으로 확인

### 1.3 비목표 (Non-goals)
- 자동 입찰 제안서 작성 기능 없음
- 공고 데이터 영구 저장·검색 기능 없음 (매주 발송만)
- 다른 강사용 SaaS 서비스로의 확장 없음 (오픈소스 템플릿 수준까지만)

---

## 2. 누가 쓰는가 (Who)

### 2.1 1차 사용자
- **Teddy (WOWD.LAB 대표)**: 17년 이상 디자인씽킹·리빙랩·AI교육·퍼실리테이션 전문가

### 2.2 2차 사용자
- **다른 기업교육 강사들**: Fork 후 `.env`의 키워드·프로필만 바꾸고 cron 표현식만 조정하면 그대로 사용 가능해야 함

---

## 3. 무엇을 만드는가 (What)

### 3.1 핵심 기능 요약

| 기능 | 설명 |
|------|------|
| 자동 수집 | 매주 월요일 KST 17:00, 공공데이터포털 API로 현재 오픈된 용역 공고 전체 수집 |
| 중복 제거 | 입찰공고번호(`bidNtceNo`) 기준 사전 제거 |
| 1차 필터 (코드) | 마감 D-14 이내 + 키워드 OR 매칭 + 예산 분기 + 대상 범위 OR 매칭 |
| AI 적합도 평가 | 1차 필터 통과 공고만 대상. Claude API 1회 호출(배치)로 강사 프로필 대비 상/중/하 + 한 줄 이유 |
| 메일 발송 | Gmail SMTP 통해 강사 본인에게 발송 |
| 0건 대응 | 적합 공고 0건 시 통계 + 아까운 공고 미리보기 안내 메일 발송 |

### 3.2 데이터 소스
- **API**: 공공데이터포털 - 조달청_나라장터 입찰공고정보서비스
- **Base URL**: `https://apis.data.go.kr/1230000/ad/BidPublicInfoService`
- **엔드포인트**: `getBidPblancListInfoServcPPSSrch` (나라장터검색조건에 의한 용역 조회)
- **조회 방식**: 키워드별 `bidNtceNm` 부분 매칭 병렬 호출, `inqryDiv=1` (공고게시일시 기준), 14일 범위
- **API 키**: 발급 완료, `.env`로 관리
- **참고 문서**: `조달청_OpenAPI참고자료_나라장터_입찰공고정보서비스_1.2.docx`

### 3.3 사용할 응답 필드 (9개)

| 필드 | 의미 | 용도 |
|------|------|------|
| `bidNtceNo` | 입찰공고번호 | 중복 제거 키 |
| `bidNtceNm` | 공고명 | 카드 제목, AI 평가 입력 |
| `ntceInsttNm` | 공고기관 | 카드 표시 |
| `dminsttNm` | 수요기관 | 카드 표시 |
| `presmptPrce` | 추정가격 | 예산 필터, 카드 표시 |
| `bidNtceDt` | 공고일시 | "신규" 배지 판정 |
| `bidClseDt` | 입찰마감일시 | 마감 임박 정렬, D-14 필터 |
| `bidNtceDtlUrl` | 원문 상세 URL | CTA 버튼 링크 |
| `ntceKindNm` | 공고종류 | 신규/정정/취소 구분 |

> **참고**: `presmptPrce`는 **원 단위** (예: 60909091 = 약 6,091만원). 0 또는 null인 공고가 일부 있음. 이 경우 "예산 미공개"로 분류.
> **참고**: `bidNtceDtlUrl`은 응답에 항상 포함됨 (형식: `https://www.g2b.go.kr/link/PNPE027_01/single/?bidPbancNo=...`).
> **참고**: `ntceKindNm`은 "등록공고", "재공고", "변경공고" 등으로 표기 (PRD 초안의 "신규/정정/취소"와 다름).

---

## 4. 필터링 로직 (전체 흐름)

### 4.1 처리 단계별 책임 분담

```
[코드 영역 - 무료/무한 처리]
   ↓
   1차 필터: 마감 D-14 이내
   2차 필터: 키워드 OR 매칭        ← AI는 키워드 매칭 안 함
   3차 필터: 예산 분기 (메인 vs 참고용)
   4차 필터: 대상 범위 OR 매칭
   ↓
[AI 영역 - 비용/품질 처리]
   ↓
   적합도 평가: 강사 프로필 대비 상/중/하 + 한 줄 이유만 판단
                (키워드 매칭은 이미 끝났음을 전제로)
```

이렇게 분리하는 이유: **단순 매칭은 코드에, 의미 판단만 AI에.** 비용·정확도·속도 모두 이득.

### 4.2 1차 필터 상세 (코드 레벨)

| 조건 | 기준 | 역할 |
|------|------|------|
| 마감 윈도우 | `bidClseDt` <= 오늘 + 14일 (`.env`로 변경 가능) | 탈락 기준 |
| 키워드 매칭 | 공고명에 "디자인씽킹", "AI교육", "리빙랩" 중 1개 이상 포함 (`.env`로 변경 가능) | 탈락 기준 |
| 예산 기준 | `presmptPrce` >= 20,000,000원 (`.env`로 변경 가능) | **태깅만** (탈락 아님) |
| 교육 대상 범위 | 공고명/공고기관에 "기업", "공공", "대학" 중 1개 이상 (`.env`로 변경 가능) | **태깅만** (탈락 아님) |

> 키워드 매칭과 대상 범위 매칭 모두 OR 조건 (하나라도 걸리면 통과).

### 4.3 필터 정책: 키워드 매칭 공고는 전부 AI 평가 대상

**변경 (v0.3)**: 키워드 매칭된 공고는 예산·대상 범위와 무관하게 **전부 AI 평가 대상**으로 올린다. 예산 미달, 대상 범위 밖은 탈락이 아니라 메일 카드에 `⚠️ 태그`로 표시한다.

**이유**: 키워드 매칭 공고가 보통 10건 미만으로 소수이기 때문에, 전부 보여주는 것이 사용자 경험에 유리. 강사가 제목이라도 보고 판단할 수 있어야 한다.

### 4.4 AI 적합도 평가 (Claude API 1회 호출)

- **모델**: `claude-haiku-4-5` (환경변수 `CLAUDE_MODEL`로 변경 가능)
- **방식**: 1차 필터 통과 메인 후보 전체를 배열로 한 번에 전달
- **AI에 전달할 정보**: 공고명, 공고기관, 수요기관, 예산, 마감일
- **AI 판단 범위**: 강사 프로필 대비 적합도(상/중/하) + 한 줄 이유
- **AI가 하지 않는 일**: 키워드 매칭, 예산 필터링, 마감일 계산 (모두 코드에서 끝남)
- **출력 포맷**: `{ bidNtceNo, score: "상|중|하", reason: "한 줄 이유" }` 배열
- **상한**: 후보 30건까지 1회 호출. 30건 초과 시 분할 호출
- **강사 프로필**: `.env`의 `INSTRUCTOR_PROFILE`로 주입

---

## 5. 메일 구조

### 5.1 적합 공고 1건 이상 케이스

```
[제목] 🎯 이번 주 입찰공고 N건 (마감 임박 X건) | YYYY-MM-DD

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 적합도 '상' 공고 N건
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[카드 1]
🆕 신규 | 적합도: ⭐⭐⭐ 상
[공고명]
공고기관 | 수요기관
💰 예산: 5,000만원
📅 마감: 2026-05-28 (D-11)
💡 적합 이유: 17년 디자인씽킹·퍼실리테이션 경험 직접 매칭
[원문 보러 가기 →]

[카드 2]
📌 진행중 | 적합도: ⭐⭐ 중
...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📎 참고용: 기준 미달이지만 키워드 적합 (M건)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- [공고명] | 1,500만원 | 마감 D-7 | [링크]
- ...
```

> **참고**: 적합 공고 1건 이상일 때는 "아까운 공고 미리보기"를 포함하지 않음. 메일 길이 관리.

### 5.2 정렬 규칙
1순위: 적합도 (상 > 중 > 하)
2순위: 마감일 임박 (D-day 오름차순)

### 5.3 배지 규칙
- 🆕 **신규**: `bidNtceDt`가 지난 7일 이내 (주 1회 발송 주기에 맞춰 윈도우 확장)
- 📌 **진행중**: 그 외

### 5.4 0건 케이스 (안내 메일)

```
[제목] 📭 이번 주 적합 공고 없음 | YYYY-MM-DD

이번 주는 적합도 '상' 또는 '중' 공고가 없습니다.

📊 이번 주 수집 통계
━━━━━━━━━━━━━━━━━━━━━━
ⓐ 전체 수집: 412건 (용역, 현재 입찰 진행 중)
ⓑ 단계별 필터링:
   - 마감 D-14 이내: 287건
   - 키워드 매칭: 8건
   - 예산 2천만원 이상: 3건
   - 대상 범위 매칭: 0건 ← 여기서 모두 탈락
ⓒ 키워드별 히트:
   - 디자인씽킹: 2건
   - AI교육: 6건
   - 리빙랩: 0건

💎 아까운 공고 미리보기
━━━━━━━━━━━━━━━━━━━━━━
- [공고명] | 1,800만원 | 마감 D-9
  💡 이유: 디자인씽킹 키워드 매칭, 단 예산 기준 미달
  [원문 보기 →]
- [공고명] | 4,500만원 | 마감 D-12
  💡 이유: AI교육 키워드 매칭, 단 대상이 초중고로 범위 밖
  [원문 보기 →]

다음 주에 다시 확인해드릴게요.
```

**아까운 공고 미리보기 선정 기준** (0건 케이스 한정):
- 키워드 매칭은 됐으나 예산·대상 범위에서 탈락한 공고 중 최대 3건
- 정렬: 예산 큰 순 → 마감 임박 순
- 이유 한 줄은 코드에서 자동 생성 (탈락 사유 명시: "예산 미달", "대상 범위 밖" 등)

---

## 6. 시스템 동작 흐름

```
┌──────────────────────────────────────────────────┐
│ GitHub Actions Cron (매주 월요일 KST 17:00)       │
│ cron: '0 8 * * 1' (UTC 기준)                      │
└──────────────────┬───────────────────────────────┘
                   ▼
┌──────────────────────────────────────────────────┐
│ Step 1. API 호출 (페이징)                         │
│  - 조달청 용역 공고 전체 (현재 진행 중)            │
│  - 응답 9개 필드만 추출                            │
└──────────────────┬───────────────────────────────┘
                   ▼
┌──────────────────────────────────────────────────┐
│ Step 2. 중복 제거                                 │
│  - bidNtceNo 기준                                 │
└──────────────────┬───────────────────────────────┘
                   ▼
┌──────────────────────────────────────────────────┐
│ Step 3. 1차 필터 (코드)                           │
│  - 마감 D-14 이내                                 │
│  - 키워드 OR 매칭                                 │
│  - 예산 분기: 2천 이상(메인) / 미만(참고용)        │
│  - 대상 범위 OR 매칭                              │
│  → 단계별 통계 누적 저장                          │
└──────────────────┬───────────────────────────────┘
                   ▼
        ┌──────────┴──────────┐
        │ 메인 후보 = 0건?     │
        └──┬───────────────┬──┘
           │ Yes           │ No
           ▼               ▼
┌──────────────────┐  ┌──────────────────────────┐
│ Step 4-a.        │  │ Step 4-b.                │
│ 아까운 공고 선정  │  │ Claude API 1회 호출       │
│ (탈락 사유 표시)  │  │ 적합도 + 한 줄 이유 배치  │
│ + 통계 메일 발송 │  └──────────────┬───────────┘
└──────────────────┘                 ▼
                     ┌──────────────────────────┐
                     │ Step 5. 정렬             │
                     │ 적합도 → 마감 임박       │
                     └──────────────┬───────────┘
                                    ▼
                     ┌──────────────────────────┐
                     │ Step 6. HTML 메일 빌드   │
                     │ + 참고용 섹션 추가       │
                     └──────────────┬───────────┘
                                    ▼
                     ┌──────────────────────────┐
                     │ Step 7. Gmail SMTP 발송  │
                     └──────────────────────────┘
```

---

## 7. 기술 스택

| 영역 | 기술 |
|------|------|
| 언어 | Node.js (18+) |
| HTTP 클라이언트 | `axios` 또는 native `fetch` |
| AI | `@anthropic-ai/sdk` (Claude API, 기본 `claude-haiku-4-5`) |
| 메일 발송 | `nodemailer` (Gmail SMTP, 앱 비밀번호 사용) |
| 환경변수 | `dotenv` |
| 스케줄 | GitHub Actions (cron, 매주 월요일 KST 17:00 = UTC 월요일 08:00) |
| 의존성 관리 | `package.json` |

---

## 8. 환경변수 명세 (.env)

```bash
# === 공공데이터포털 API ===
G2B_API_KEY=your_decoded_api_key_here
G2B_BASE_URL=https://apis.data.go.kr/1230000/ad/BidPublicInfoService

# === Claude API ===
ANTHROPIC_API_KEY=sk-ant-xxxxx
CLAUDE_MODEL=claude-haiku-4-5

# === Gmail SMTP ===
GMAIL_USER=your_email@gmail.com
GMAIL_APP_PASSWORD=xxxx_xxxx_xxxx_xxxx
MAIL_TO=your_email@gmail.com

# === 필터링 기준 (다른 강사가 바꿀 부분) ===
KEYWORDS=디자인씽킹,AI교육,리빙랩
TARGET_SCOPE=기업,공공,대학
MIN_BUDGET=20000000
DEADLINE_WINDOW_DAYS=14

# === 강사 프로필 (AI 평가용) ===
INSTRUCTOR_PROFILE="17년 이상의 디자인씽킹 교육 훈련 전문가, 리빙랩 전문가, 퍼실리테이터, AI 교육 전문가"
```

> **발송 주기 변경**: `.github/workflows/weekly.yml`의 cron 표현식을 직접 수정.
> - 매주 월요일 17시 KST: `'0 8 * * 1'`
> - 매일 17시 KST: `'0 8 * * *'`
> - 월/수/금 17시 KST: `'0 8 * * 1,3,5'`
> - 화/목 17시 KST: `'0 8 * * 2,4'`

`.env.example` 파일에는 위 키만 빈 값으로 포함.

---

## 9. 파일 구조 (예정)

```
g2b-bid-scout/
├── .env                  # gitignore
├── .env.example
├── .github/
│   └── workflows/
│       └── weekly.yml    # GitHub Actions cron (주 1회 기본)
├── src/
│   ├── index.js          # 진입점
│   ├── fetcher.js        # 공공데이터 API 호출 + 페이징
│   ├── filter.js         # 1차 필터링 + 분기 + 통계 누적
│   ├── evaluator.js      # Claude API 적합도 평가
│   ├── mailer.js         # Nodemailer + HTML 템플릿
│   └── utils.js          # 날짜·통계·중복제거 헬퍼
├── package.json
├── PRD.md                # 이 문서
├── CLAUDE.md             # 작업 규칙
├── README.md             # 사용자 가이드
└── current_status.md     # 세션 메모리
```

---

## 10. 성공 기준 (Success Metrics)

| 항목 | 기준 |
|------|------|
| 정확도 | 적합도 '상' 공고 중 강사가 "실제 제안 검토하겠다"고 응답한 비율 ≥ 50% (4주 운영 후) |
| 누락률 | 강사가 별도 수동 확인했을 때 놓친 적합 공고 ≤ 1건/월 |
| 안정성 | 4주 연속 메일 발송 성공률 100% |
| 비용 | Claude API 주간 비용 ≤ 100원 (haiku 1회 호출 + 30건 평가 기준) |
| 이식성 | 다른 강사가 README만 보고 30분 내 본인 환경에 배포 가능 |

---

## 11. 리스크 및 결정 보류 항목

| 리스크 | 대응 |
|--------|------|
| 공공데이터 API 응답 지연/오류 | 3회 재시도 + 실패 시 에러 메일 발송 |
| Claude API 토큰 한계 (30건 초과) | 분할 호출 로직 사전 구현 |
| 키워드 매칭 누락 (예: "디자인 씽킹" 띄어쓰기) | PPSSrch 부분 매칭으로 일부 해소. v1.1에서 유사어 사전 도입 검토 |
| Gmail SMTP 발송 한도 | 주 1회 발송이므로 무관 |
| 정정공고 처리 | `ntceKindNm` 표시만 (등록공고/재공고/변경공고), 자동 매칭은 v1.1 |

### 결정 보류 (v1.1 이상에서 다룰 것)
- 키워드 유사어 사전 (디자인씽킹 vs 디자인 씽킹 vs Design Thinking)
- 정정공고와 원공고 자동 매칭
- 강사 피드백 루프 (메일에서 "이 공고 적합했다/아니었다" 클릭 → 학습)
- 영문 공고명 대응

---

## 12. 다음 단계

1. ✅ PRD.md v0.2 확정 (현재 문서)
2. ⏭ CLAUDE.md 작성 (작업 규칙·코딩 컨벤션)
3. ⏭ README.md 작성 (Fork → Secrets 등록 → 키워드 수정 가이드, cron 변경 예시)
4. ⏭ current_status.md 작성 (세션 메모리)
5. ⏭ 코드 구현 시작 (`src/fetcher.js`부터)

