Skip to content

feat(funding): add funding:sync with automation settings and source freshness - #37

Merged
jhjh0512 merged 14 commits into
mainfrom
feat/funding-sync
Sep 28, 2026
Merged

jhjh0512 merged 14 commits into
mainfrom
feat/funding-sync

Conversation

@jhjh0512

Copy link
Copy Markdown
Contributor

관련: ADR 0006(#33) 2·4·5·6·7절. 별도 Issue는 없다.

#36 위에 쌓은 PR이다(#33 → #36 → 이 PR). 두 PR이 병합되면 GitHub가 base를 main으로 바꾸고, 그 뒤 main을 합쳐 diff를 정리한다. #33·#36보다 먼저 병합하지 않는다.

소진공 정책자금 크롤러 가운데 출처와 상관없는 절반이다. 이 작업 환경은 네트워크 정책 때문에 ols.semas.or.kr에 접속하지 못해, 실제 HTML이 필요한 수집기(파서)는 다음 PR로 나눴다. 이 PR은 수집기를 FundingSourceAdapter로 꽂기만 하면 되도록 동기화·판정·저장을 준비한다.

변경 내용

  • 카탈로그 스키마 funding-catalog-v1.1.0
    • 상품에 sourceRef(출처·공고 식별자)를 추가했다. 기본값을 두지 않아서, 이 필드가 없는 기존 상품 버전의 저장 내용은 바뀌지 않는다.
    • 카탈로그에 automation을 추가했다. 정책자금 자동 승격·상환 계산 자동 승격 스위치, 차단 목록, 출처별 동기화 결과를 담는다. 두 스위치는 기본으로 꺼져 있다.
    • v1.0.0으로 적재된 릴리스도 계속 읽는다. 이때 자동화 설정은 모두 꺼진 기본값이다. 배포 직후에도 지금 ACTIVE 릴리스가 그대로 동작한다.
    • 새 규칙 검사는 다음을 오류로 본다: 출처 식별자 없는 자동 검수자(AUTO:…), 같은 공고를 가리키는 상품 두 개, 같은 출처의 동기화 결과 두 개. 자동 상품은 다음 검토일이 없어도 된다.
  • DB: 새 migration으로 funding_catalog_releases.automation(JSONB, nullable)을 추가하고, 적재기가 이 열을 저장한다.
  • 판정(eligibility.ts, types.ts): 검수자가 AUTO:funding-auto-v1인 상품에만 적용한다.
    • 다음 검토일 대신 출처의 마지막 성공 동기화일로 최신성을 본다. 그날에서 3일이 지났거나 성공 기록이 없으면 REVIEW_OVERDUE이고, 사유에 출처와 마지막 성공일을 적는다.
    • 다음 세 경우는 NEEDS_CONFIRMATION으로 두고 사유를 표시한다: 차단 목록의 공고, 지원사업(지원금·공간·프로그램) 자동 상품, 정책자금 자동 승격이 꺼진 상태.
    • 상환 계산 자동 승격이 꺼져 있으면, 확정 조건이 있어도 상환 계산 대상이 아니다.
    • 사람이 기록한 상품은 sourceRef가 있어도 지금 규칙을 그대로 따른다.
  • 대출 가정 적용 API: 활성 릴리스의 자동화 설정으로 판정한다. 상환 가능 여부도 상품이 아니라 판정 결과(evaluation.repayment)로 확인한다. 이렇게 해서 상환 계산 스위치를 우회할 수 없게 했다.
  • funding:sync(sync.ts, scripts/sync-funding.ts)
    • 출처별 수집기로 공고를 모아 catalog.json의 상품과 합친 뒤, 기존 적재기(PENDING → ACTIVE)로 적재한다.
    • 상품 버전: 날짜와 근거 checksum만 다른 상품은 이미 적재된 불변 버전을 재사용한다. 내용이 바뀌면 PATCH를 올린다.
    • 출처 실패: 수집기 오류, 빈 응답, 직전 성공 대비 절반 미만, 규칙 검사 실패는 그 출처만 실패로 기록한다. 그 출처의 상품은 직전 ACTIVE 릴리스의 것을 그대로 두고, 마지막 성공일도 그대로 둔다. 빈 응답을 "공고 없음"으로 해석하지 않는다.
    • 사람이 기록한 상품이 같은 공고(sourceRef)를 가리키면 자동 상품을 뺀다. catalog.json에 자동 검수자 상품이 있으면 거부한다.
    • catalogVersion은 동기화한 날.번호다. 같은 날 적재된 릴리스와 catalog.json의 버전을 피해 번호를 올린다. 이렇게 해야 같은 날 funding:load와 충돌하지 않는다.
    • 합친 카탈로그는 data/funding-sync/(Git 제외)에 남는다. --dry-run은 파일을 쓰고 검증만 한다.
    • 등록된 수집기는 아직 없다. 이 상태에서 실행하면 catalog.json만 새 릴리스로 적재한다.
  • catalog.json: v1.1.0 2026-09-28.1로 올리고 automation을 명시했다(두 스위치 모두 꺼짐). 상품 내용과 상품 버전(1.0.1)은 바뀌지 않는다.
  • 문서
    • AGENTS.md §3의 8번: "미검수"를 "사람 검수나 ADR 0006의 자동 확인을 거치지 않은"으로 바꿨다(ADR 9절).
    • docs/domains/funding.md: 스키마, 동기화 규칙, 자동 상품 판정, 수집 출처와 허용 확인 기록(소진공 2026-09-28)을 적었다.
    • DEVELOPMENT.md: funding:sync 실행 방법을 적었다.
    • 카탈로그 README: 자동화 설정 운영법을 적었다.
    • ARCHITECTURE: 테스트 목록에 sync.test.ts를 더했다.

검증

  • npm --prefix app run verify:fast 통과: 카탈로그 검증, Prettier, ESLint, TypeScript, 테스트, build.
    • Vitest: 253 passed, 40 skipped. 건너뛴 테스트는 PostgreSQL이 필요한 묶음 34개와 원본 대조 테스트 6개다. 이전보다 늘어난 8개는 이 PR이 추가한 sync.test.ts의 DB 테스트다.
  • DB 테스트: 이 작업 환경에 Docker Compose(PostgreSQL 17)가 없어 verify:db는 돌리지 못했다. 대신 로컬 PostgreSQL 16에 migration을 적용하고 DB 모드를 직접 실행했다(run-tests.ts database). 287 passed, 6 skipped(data/raw/ 없음)였다.
    • prisma migrate diff --from-config-datasource --to-schema는 빈 migration이었다. migration과 schema가 일치한다는 뜻이다.
  • E2E(Playwright): desktop·mobile 6개가 모두 통과했다(같은 PostgreSQL 16 테스트 DB). 작업 환경에 미리 설치된 Chromium을 임시 설정으로 지정해 실행했고, 설정 파일은 바꾸지 않았다.
  • CLI: 별도 DB에서 funding:sync --dry-run과 실제 적재를 실행했다. 사람이 기록한 상품 5건이 ACTIVE 릴리스로 적재됐다.
  • 재현과 회귀
    • 새 판정 테스트는 모두 새 규칙에만 있는 사유 문자열을 확인한다.
    • 대출 가정 테스트는 이전 코드에서 실패한다. 자동화 설정을 넘기지 않으면 "켜짐"도 400이 되고, 판정 결과로 상환 가능 여부를 확인하지 않으면 "꺼짐"이 200이 된다.
  • 로컬 권장: PostgreSQL 17에서 npm --prefix app run verify를 한 번 돌려 보기를 권한다.

위험·후속

  • DB 변경: nullable 열 추가뿐이다. 기존 릴리스는 NULL이고, 읽을 때 모두 꺼진 기본값으로 해석한다.
  • 스키마 버전: v1.1.0으로 적재한 릴리스를 이전 앱이 읽으면 "지원하지 않는 스키마" 오류가 난다. 그래서 배포 뒤에 적재해야 한다.
  • 운영 명령 전환: 수집기가 등록되면 funding:load가 아니라 funding:sync를 써야 한다. funding:load로 catalog.json만 적재하면 자동 상품이 빠진다(카탈로그 README).
  • 후속
    • 소진공 정책자금 수집기(ols.semas.or.kr 파서, 크롤링 규칙: 하루 한 번, 1초 간격, User-Agent 연락처, 403·429·CAPTCHA 시 중단)를 등록하는 PR이 이어진다. 이 환경에서 그 호스트에 접속할 수 있어야 한다.
    • 후보 화면에 "자동 확인"·마지막 확인일을 표시하는 일과, 두 갈래로 나누는 일은 별도 PR로 한다.
    • 정기 실행 위치는 #15에서 정한다.

🤖 Generated with Claude Code

https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL


Generated by Claude Code

claude and others added 13 commits September 25, 2026 08:10
Draft ADR 0006 (Proposed) for issue #5. Discovery from the K-Startup
and Bizinfo Open APIs produces catalog drafts with reviewer UNASSIGNED;
demotions (application end date passed, source changed, removed or
closed) apply automatically; promotion to a current candidate still
needs a human-reviewed catalog release. AI interpretation of
announcement text stays out of scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
Rework the draft per the decision to automate both directions:
funding:sync builds the catalog from the K-Startup and Bizinfo APIs and
promotes non-loan announcements whose dates, stage and region come from
structured fields. Industry and purpose UNKNOWN become pre-application
checks instead of blocking. Loans and guarantees stay human-reviewed.
Source failures keep the current release, and auto products drop out
when syncs stop for more than three days.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
Support programs (grant, space, program) are automated from the
K-Startup API. Policy loans and guarantees have no official API, so
they are automated by crawling the SEMAS and Seoul Shinbo pages under
explicit rules: robots.txt and terms checked per source, public pages
only, once a day, no evasion when blocked. Repayment calculation is
auto-promoted only when the page states a single fixed rate, term,
grace and method; variable-rate loans stay non-calculable. Each track
has its own promotion switch, and a failing source keeps its last
products until they go stale after three days.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
Mark ADR 0006 Accepted with jhjh0512 as owner, and state that the
follow-up doc changes in section 9 land with the implementation PRs
(AGENTS.md section 7), not with this decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
…hecks

Per ADR 0006, industry and purpose are conditions no official source
gives in structured form, so an UNKNOWN verdict on them no longer keeps
a product out of the current candidates. They are added to manualChecks
as "업종: ..." / "용도: ..." and counted in the candidate reason, while
eligibilityVerdict stays UNKNOWN. An UNKNOWN business stage or region
still means NEEDS_CONFIRMATION, and any FAIL still means NOT_ELIGIBLE.

AGENTS.md section 3 item 8 now points to ADR 0006 for this case, and
funding.md and the candidate group notes describe the new split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
Record the 2026-09-28 checks of real API responses, robots.txt and terms.
K-Startup would surface 116 mostly unrelated current candidates for a Seoul
pre-founder, so support programs are discovered through Bizinfo and promoted
only by a person. Policy loans are crawled from the SEMAS policy fund site,
including its notice list JSON to locate notice pages. Sbiz24's internal API
is not used, and Seoul development funds stay human-managed because their
terms exist only in HWP attachments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reshness

Implements the source-agnostic half of ADR 0006 so a policy-fund crawler
can plug in as a FundingSourceAdapter.

- Catalog schema funding-catalog-v1.1.0 adds sourceRef on products and an
  automation block (policy loan and repayment promotion switches, blocked
  sources, per-source sync results). v1.0.0 releases stay readable with
  every switch off. The release stores automation in a new JSONB column.
- Automated products (reviewer AUTO:funding-auto-v1) are judged by the
  source's last successful sync instead of nextReviewAt and drop to
  REVIEW_OVERDUE after three days. Blocked notices, automated support
  programs, and promotion switched off keep them in NEEDS_CONFIRMATION
  with the reason. The repayment switch gates repayment support, and the
  loan-assumption route now checks the evaluation instead of the product.
- funding:sync collects per source, reuses immutable product versions
  when only dates change, keeps the previous products and last success
  date when a source fails (error, empty, drop below half, validation),
  lets a manual record override the same notice, and loads the merged
  catalog through the existing PENDING to ACTIVE loader. No source
  adapter is registered yet.
- catalog.json moves to v1.1.0 as 2026-09-28.1 with both switches off;
  product versions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
Base automatically changed from feat/funding-pre-application-checks to main September 28, 2026 14:28
Resolve AGENTS.md and eligibility.ts in favor of this branch: main holds
the squashed #36 version, and this branch already contains #36 plus the
#37 changes on the same lines. The merged tree is identical to ba3ac46.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LCE4DgT5qdHP8y8V5qtbL
@jhjh0512
jhjh0512 merged commit 7ee6af0 into main Sep 28, 2026
1 check passed
@jhjh0512
jhjh0512 deleted the feat/funding-sync branch September 28, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants