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.pyTODO(의미검색). 기존 라우터의 _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.ymlnpm ci → lint → format:check → build(타입체크 포함)
  • discord-pr-notify.yml — PR 열림·머지·리뷰·댓글·새 커밋 알림

백엔드 Discord는 조용할 수 있다. 백엔드 워크플로엔 시크릿 가드가 없어서, 웹훅이 죽어 있어도 job은 그냥 통과한다 (curl -s는 404에 실패하지 않는다). 알림이 안 오면 이걸 의심할 것.


8. 다음 할 일

  1. 추천 엔진 기준 합의 — 목업 3화면이 여기 묶여 있다. 팀 논의 필요
  2. 프론트 테스트 도입 — 백엔드 의존 없이 지금 가능
  3. 의미 검색 승격 (백엔드, 마커 남김)
  4. 페이지네이션

쿼리 파이프라인 · 쿼리 파이프라인 개선안 · 검색 실험 구도