Work
Full-Stack · Civic2026

청년정책 레이더

맞춤형 복지·지원 정책 알림 시스템

기획 · 풀스택 · 운영

받을 수 있는 지원사업을 몰라서 놓치는 게 아까워서 만들었습니다. 어려웠던 건 수집이 아니라 판정이었습니다 — 정부 API는 자격 조건을 코드값으로만 주고, 행정구역 개편으로 지역코드가 뒤섞여 있고, 절반 이상은 자유 텍스트라 기계가 읽을 수 없습니다. 그래서 '자격이 있다'고 단정하는 대신 '확인 필요'를 기본값으로 두고, 모르는 것을 아는 척하지 않는 쪽으로 엔진 전체를 설계했습니다.

2,698건

실 응답으로 검증한 공고

50건

판정 엔진 회귀 테스트

0

판정 경로의 LLM 호출

풀어야 했던 문제

  • 온통청년 API가 취업상태·학력·혼인을 한글명 없이 코드값으로만 준다 (전수 2,698건 중 한글명 0건)
  • 행정구역 개편으로 법정동코드가 신·구 혼재 — 강원 42xxx→51xxx, 전북 45xxx→52xxx, 광주+전남은 12xxx로 재배정
  • 자격 조건 상당수가 자유 텍스트라 규칙으로 판정할 수 없다
  • 조건을 만족해도 이미 받는 제도와 중복수급 제한이 걸릴 수 있다

판정 엔진 — 틀리기보다 모른다고 말하게

  • 판정 5단계: ✅자격 충족 · ❓확인 필요(기본값) · ⚠중복수급 주의 · ⬜해당 없음 · ⏹마감
  • LLM을 판정 경로에서 완전히 배제 — 여기서 환각이 나면 사람이 신청 기회를 잃는다. 요약·자유텍스트 구조화에만 사용
  • 빈 배열을 '제한 없음'으로 읽지 않는다 — 값이 없는 건 '조건이 없다'가 아니라 '모른다'. 이걸 통과로 처리했다가 자격 미달자에게 '자격 충족'이 뜬 적이 있다
  • 모든 판정에 근거(reasons)를 남겨 사용자에게 그대로 노출 — 어떤 조건이 왜 통과·실패했는지

지역코드 정규화

  • 시도 prefix만 바뀐 개편(강원·전북)은 양방향 기계 변환
  • 시군구가 재배정된 개편(광주+전남 통합)은 변환식이 없다 — 목포는 46110→12110으로 뒷자리 유지, 광양은 46230→12190으로 밀림
  • 확인된 매핑만 표에 명시하고, 표에 없는 구 코드는 '대상 아님'이 아니라 unknown 처리 — 모르는 걸 단정하면 받을 수 있는 제도를 조용히 놓친다

관련성 층 — 판정과 분리

  • 첫 실발송에서 '자격 충족' 63건이 그대로 실렸는데, 20건 넘게가 '○○센터 운영', '△△대회 개최' 같은 기관 예산 항목이었다
  • 지역으로 거를 수 없었다 — 인구유입형 사업은 오히려 해당 지역을 대상에서 빼고 있었다. 그래서 '어디 사업인가'가 아니라 '무엇을 주는 사업인가'로 분류
  • 현금성 100 · 기회형 50 · 서비스형 10 가중에 마감 임박(+25)·관심사 일치(+30)를 더해 정렬, 메일당 12건 상한
  • 관련성으로 판정을 건드리지 않는다 — 밀려난 항목은 대시보드에 남고 다음 주 후보로 되돌아온다

수집 · 운영

  • 원본 응답(rawDetail)을 통째로 보존 — 정부 API는 예고 없이 필드명을 바꾸므로 파싱이 실패해도 원본은 남아야 한다
  • contentHash로 변경분만 감지, CollectRun에 소스별 성패를 기록해 한 소스가 죽어도 나머지는 저장
  • Docker Compose 배포 + 호스트 crontab(매일 수집 / 주 1회 발송) — 크론을 컨테이너 밖에 두어 실패 시 종료코드가 드러나게
  • 대시보드는 127.0.0.1에만 바인딩 — 생년월일·주소·수혜이력이 담기므로 공개 인터넷에 노출하지 않는다

Stack

Next.js 16TypeScriptPrismaPostgreSQL공공데이터 APIResendDocker Composecron
NextSecretVault