BidMate 웹 연동 현황
프론트(GPTPilots_Webs)와 백엔드(GPTPilots_Project)를 붙인 작업의 결과와 남은 일.
질의 흐름 자체는 쿼리 파이프라인 참고.
한 줄 요약: 공고 목록·상세는 실데이터로 살아났고, 남은 3화면은 추천 엔진이 없어서 못 붙인다.
화면 구조가 바뀌었다 (2026-07-29)
공고 목록(
/recommendations)과 상세(/rfp/:id)는 한 화면으로 합쳐졌다. 아래 화면별 서술은 합치기 전 기준이다. 현행 동작·실측은 작업 화면 개편 실험 참고.
1. 무엇이 문제였나
와이어프레임 5화면이 전부 목업이었다. 원인을 프론트 배선으로 의심했는데 아니었다.
백엔드에 노출된 API가
POST /ask하나뿐이었다. 공고 목록·상세를 채울 조회 API가 애초에 없었다.
그래서 프론트를 고치는 게 아니라 백엔드에 API를 먼저 신설했다.
2. 만든 것 — 공고 카드 API
GET /rfps (목록·검색·페이지네이션) / GET /rfps/{doc_id} (단건)
| 항목 | 내용 |
|---|---|
| 출처 | data/extracted/meta.json 100건 |
| 비용 | 0 — 임베딩·Chroma·LLM을 안 탄다 (검색이 아니라 열람) |
| 응답 | doc_id · 공고번호 · 사업명 · 발주기관 · 금액 · 공개일 · 마감일 · 사업요약 · 파일형식 |
| 보안 | /ask와 동일한 공유 토큰·rate 가드, 없는 id는 404 |
| NDA | 사업요약에 마스킹 적용, source_file(로컬 절대경로)·파일명은 계약에서 제외 |
비용이 0이라는 게 중요하다. 목록을 아무리 넘겨도 돈이 안 든다. 돈 드는 건 채팅뿐.
3. 화면별 상태
| 경로 | 화면 | 상태 |
|---|---|---|
/recommendations | 공고 목록 | 실데이터 — 100건, 마감일 최신순, 검색 |
/rfp/:doc_id | 공고 상세 | 실데이터 + 채팅 |
/onboarding | 온보딩 | 목업 |
/questionnaire | 질문지 | 목업 |
/me | 마이페이지 | 목업 |
상세 화면에서 달라진 점
해당 공고가 활성 문서로 고정된다. 그래서 사업명을 안 써도 된다.
- 이전:
"<사업명>의 주요 과업 내용 알려줘"→ 사업명 빠지면 반문 - 지금:
"주요 과업 내용은?"→ 바로 근거 5건과 함께 답변 (실측 $0.0017)
4. 일부러 안 한 것
별점·적합도·태그를 되살리지 않았다.
목업엔 ⭐⭐⭐ 적합도 상 같은 게 있었지만 판정 기준이 정해진 게 없다. 실적 도메인 매칭인지, 금액대인지, 기관 유형인지 아무것도 합의된 바 없다.
실데이터 옆에 근거 없는 점수를 붙이면 사용자가 진짜 추천으로 오해한다. 추천 엔진이 생기기 전까지 되살리지 말 것.
마이페이지 “이어서 대화하기”도 같은 이유로 비활성화했다 — 대화 이력 저장 백엔드가 없어 죽은 링크가 된다.
5. 미해결
백엔드가 없어서 막힌 것
① 추천 엔진 — 기준 미정 (가장 큰 블로커) 목업 3화면이 전부 여기 걸려 있다. 막힌 건 구현이 아니라 정의다. “적합도 상/중/하”가 뭔지 먼저 합의해야 한다. 온보딩·질문지가 뭘 수집할지도 여기서 갈린다.
② 대화 이력 저장 없음 마이페이지 “지난 대화”가 목업으로 남는 이유.
③ 검색이 문자열 부분일치
"재난"은 찾지만 "CCTV 영상분석 관련 사업" 같은 자연어는 0건.
승격 방법은 이미 코드에 적어뒀다 — src/api/cards.py의 TODO(의미검색).
기존 라우터의 _default_search_doc_cards를 재사용하면 되고, 로컬 E5라 비용 0.
/rfps 계약이 안 바뀌므로 프론트는 수정 불필요.
프론트만으로 할 수 있는 것
④ 페이지네이션 없음
PAGE_SIZE = 20 고정. 100건 중 20건만 보이고 나머지는 검색으로 좁혀야 한다.
백엔드가 offset을 이미 받으므로 프론트만 붙이면 된다.
⑤ 테스트 0개 ← 우선 권장
src/ 전체에 테스트 파일이 없다. CI는 lint·format·build(타입체크)만 돈다.
아래 6번의 크래시 버그도 테스트가 있었으면 CI에서 걸렸을 종류다.
format.ts(금액·D-day), api.ts(에러 분류) 같은 순수 함수부터 붙이면 값이 크다.
⑥ 배포 없음 로컬 전용. 백엔드가 NDA 경계상 로컬/터널 뒤 구동 전제라 프론트만 올려도 의미가 없다.
6. 리뷰에서 잡은 버그 — doc_id 이중 디코딩
머지 전 리뷰에서 나온 것. 기록해둘 가치가 있다.
useParams는 이미 디코딩된 값을 돌려주는데 거기에 decodeURIComponent를 한 번 더 걸고 있었다.
/rfp/%25 → URIError: URI malformed → 상세 화면 통째로 죽음
현재 100건엔 %가 없어 드러나지 않았지만, "100% 국비" 같은 사업명이 들어오는 순간 터진다.
규칙
- URL을 만들 때:
encodeURIComponent로 감싼다 - URL에서 읽을 때:
useParams값을 그대로 쓴다 (react-router가 이미 디코딩함)
7. 머지 상태
| 레포 | PR | 결과 |
|---|---|---|
GPTPilots_Project | #20 공고 카드 API | 머지됨 |
GPTPilots_Webs | #2 GitHub Actions 도입 | 머지됨 |
GPTPilots_Webs | #3 목록·상세 실데이터 연동 | 머지됨 |
프론트에 워크플로가 하나도 없어서 백엔드와 같은 방식으로 맞췄다.
ci.yml—npm ci→ lint → format:check → build(타입체크 포함)discord-pr-notify.yml— PR 열림·머지·리뷰·댓글·새 커밋 알림
백엔드 Discord는 조용할 수 있다. 백엔드 워크플로엔 시크릿 가드가 없어서, 웹훅이 죽어 있어도 job은 그냥 통과한다 (
curl -s는 404에 실패하지 않는다). 알림이 안 오면 이걸 의심할 것.
8. 다음 할 일
- 추천 엔진 기준 합의 — 목업 3화면이 여기 묶여 있다. 팀 논의 필요
- 프론트 테스트 도입 — 백엔드 의존 없이 지금 가능
- 의미 검색 승격 (백엔드, 마커 남김)
- 페이지네이션