SP가 스스로 낡지 않게 만드는 세 개의 자동 갱신 파이프라인
혼디의 SP(Service Persona)는 시간이 지나면 수치·법령·연락처가 바뀌어 낡는다. 사람이 수백 개의 SP를 매번 순회하며 낡은 곳을 찾는 건 비현실적이라, “무엇을 갱신해야 하는지 찾아서 큐에 올리는 일”만 자동화했다. 실제 문안 작성과 반영 승인은 이 문서 전체에서 예외 없이 사람(주피터님) 몫이다.
worker.js(hondi-proxy)는 무상태(stateless) HTTP 핸들러만 제공하고,
저장소 안에는 크론(cron)이 존재하지 않는다는 원칙을 따른다. 하지만 SP는
가만히 둬도 낡는다 — 방문 시간, 수수료, 담당 부서 연락처 같은 정보는
시간이 지나면 실제와 어긋난다. 이 어긋남을 사람이 매번 발견해서 고치는
구조로는 SP 개수가 늘어날수록(현재 전국 16개 광역시도 + 시군구 + 국가기관 +
K-서비스 전체) 감당이 안 된다.
그래서 GitHub Actions의 schedule 트리거를 “저장소 밖 스케줄러”로
삼아 세 개의 자동 갱신 파이프라인을 돌린다. 셋 다 “갱신이 필요한
대상을 찾아서 사람이 검토할 큐에 올리는 것”까지만 하고, 실제 SP
파일 수정·반영은 절대 자동으로 하지 않는다.
flowchart LR
A["GitHub Actions\nschedule 트리거"] --> B1["① SP Refresh\nScheduler"]
A --> B2["② 미등록 태그\n반복 집계"]
A --> C["③ 사용자 피드백\n취합"]
B1 --> Q1[("sp-author/queue")]
B2 --> Q1
C --> Q2[("sp_update_proposals")]
Q1 --> H["사람 검토·승인"]
Q2 --> H
H --> SP[("SP 원문 갱신")]
| 파이프라인 | 워크플로 파일 | 주기 | 찾아내는 것 |
|---|---|---|---|
| ① SP Refresh Scheduler | sp-refresh-scheduler.yml |
매일 09:00 KST | 갱신 주기(tier)가 도래한 SP |
| ② 미등록 태그 집계 | sp-refresh-scheduler.yml(동일 워크플로 2번째 스텝) |
매일 09:00 KST | 여러 사용자가 반복해서 걸리는 미등록 전문가·서비스 태그 |
| ③ 사용자 피드백 취합 | triage-user-feedback.yml |
매주 월 07:00 KST | 여러 사용자가 비슷하게 지적한 SP 오류·개선 요청 |
세 파이프라인 모두 동일한 경계선을 지킨다.
- 자동으로 하는 것: 조회 · 계산 · 클러스터링 · 큐잉 · (③에 한해) 패치 초안 작성
- 자동으로 하지 않는 것: SP 파일 직접 수정,
sp_update_proposals상태를approved로 변경, 반영 배포
실행 파일
- 워크플로:
.github/workflows/sp-refresh-scheduler.yml - 스크립트:
tools/sp_refresh_scheduler.py
tier 배정 기준
gwp_registry의 call_count_30d(최근 30일 호출 횟수)를
절대 임계값과 비교해 갱신 주기를 재배정한다 (실시간 퍼센타일이 아니라
docs/SP-AUTHOR-AUTOMATION_v1_0.md §2-2의 고정 수치를 그대로 쓴다 —
항목 수가 적을 때 퍼센타일은 쉽게 왜곡되기 때문).
| tier | 기준(call_count_30d) | 재검토 주기 | 예시 |
|---|---|---|---|
weekly | 200회 이상 | 7일 | K-Health·K-Market·kgov/jeju 등 고빈도 서비스 |
monthly | 20~199회 | 30일 | 대부분의 K-서비스·행정시/읍면동 SP |
quarterly | 20회 미만 | 90일 | 저빈도·특수 절차 SP |
호출 이력이 아예 없거나 0인 신규 SP는 과소추정을 막기 위해
기본값 monthly로 둔다 — “안 써서 낮은 등급”으로 오판하지 않는다.
실행 흐름
flowchart TD
A["GET /gwp-registry/search\nactive 항목 전체 조회"] --> B{"call_count_30d\n임계값 비교"}
B -->|"≥200"| W["tier = weekly"]
B -->|"20~199"| M["tier = monthly"]
B -->|"<20"| Q["tier = quarterly"]
B -->|"0 / 없음"| M2["tier = monthly\n(과소추정 방지 기본값)"]
W & M & Q & M2 --> R["POST /sp-author/refresh-schedule"]
R --> D["GET /sp-author/refresh-due\n주기 도래 항목 조회"]
D --> QU["POST /sp-author/queue\npriority: low"]
QU -.->|"큐잉만, 자동 수정 없음"| H(["사람 검토"])
큐잉 우선순위는 항상 low다 — 실사용자가 직접 신호를 보낸
경우(②·③)보다 후순위로 둔다는 것이 SP-AUTHOR-AUTOMATION_v1_0.md의
설계 원칙이다.
실행 파일
tools/aggregate_unresolved_tags.py — tier 재계산과 관심사가 달라
(하나는 기존 SP 갱신 주기, 하나는 신규 미등록 수요 발굴) 별도 스크립트로
유지하되, 실행 주기(매일 1회)는 공유해 별도 워크플로를 새로 만들지 않았다.
문제의식
expert-session.js의 _reportUnresolvedTag가
EXPERT/GWP 태그 해석 실패(예: “연금회계사”처럼 아직 등록 안 된 전문가
분류)마다 큐에 기록한다. 한 사람이 한 번 걸리면 그 한 건만 남지만,
여러 사람이 같은 미등록 항목에 반복해서 걸리면 그건 “진짜 수요”라는
신호다 — 다만 개별 레코드로만 쌓이면 그 신호가 묻힌다.
실행 흐름
flowchart TD
A["GET /sp-author/queue?status=queued"] --> B["signal_source ∈\n{expert_tag_resolution,\ngwp_tag_resolution} 필터"]
B --> C["최근 7일 이내\n생성분만 대상"]
C --> D["institution(raw_id)\n기준 그룹화"]
D --> E{"그룹 크기\n≥ 3건?"}
E -->|"아니오"| F["보류 — 리포트만"]
E -->|"예"| G["대표 레코드 1건 유지\n나머지 status=duplicate"]
G --> P["대표 priority → high"]
P --> N["POST /sp-author/escalate\n(reason: sp_draft_request)"]
N -.->|"알림만, 자동 작성 없음"| H(["사람이 신규 SP 작성"])
기본 임계값은 7일 · 3건이며, --days/--threshold
옵션으로 조정할 수 있다.
실행 파일
- 워크플로:
.github/workflows/triage-user-feedback.yml - 스크립트:
tools/triage_feedback.py - 출력 리포트:
data/feedback_triage_report.json(사람이 읽는 요약, git 커밋 대상)
세 파이프라인 중 유일하게 실제 문안 초안까지 자동으로 만든다. 그래도 승인은 여전히 사람만 한다 — 이번 변경은 “검토하기 더 쉬운 초안을 주는 것”이지 “검토를 생략하는 것”이 아니다.
실행 흐름
sequenceDiagram
participant CI as GitHub Actions
participant PB as L1 PocketBase
participant W as worker.js
participant LLM as DeepSeek V4 Flash
CI->>PB: user_feedback (status=new) 조회
CI->>W: POST /embed-text (bge-m3 임베딩)
CI->>CI: 코사인 유사도 클러스터링 (threshold 0.82)
Note over CI: 브릿지 조건 검사
크기≥3건 · context_sp 단일 · bug/feature_request
alt 조건 충족
CI->>W: POST /sp-updates/draft-patch
W->>LLM: SP 원문 참고 초안 요청
LLM-->>W: 패치 초안
W-->>CI: draft, confidence
CI->>PB: POST /sp-updates/propose (status=pending_review)
else 조건 미충족
CI->>CI: 리포트에만 기록 (큐잉 없음)
end
CI->>PB: data/feedback_triage_report.json 커밋
| 브릿지 조건 | 기준 | 이유 |
|---|---|---|
| 클러스터 크기 | ≥ 3건 | 표본 부족으로 인한 과신 방지 |
| context_sp | 단일 | 대상 SP가 모호하지 않아야 함 |
| category | bug 또는 feature_request | 칭찬·단순 질문은 SP 수정 제안거리가 아님 |
초안 작성이 네트워크 오류 등으로 실패해도 조용히 플레이스홀더
문구로 폴백한다(confidence: low) — 초안 생성 실패가
클러스터링·리포트 생성 자체를 막으면 안 되기 때문이다. 초안이 실제로
만들어지면 confidence: medium으로 표시하지만, 이 값도
어디까지나 참고용이고 사람 검토를 대신하지 않는다.
user_feedback 컬렉션 스키마는 worker.js의
handleUserFeedbackSubmit() payload를 기준으로 추정했고,
클러스터링 임계값(0.82)도 실측 데이터 없이 잡은 잠정값이다. 실제
피드백이 쌓이면 재조정이 필요할 가능성이 높아, 매 실행마다 리포트에
클러스터 크기 분포를 남겨 조정 근거로 삼는다.
세 파이프라인이 만들어낸 신호는 결국 두 곳 중 하나로 모인다 —
sp-author/queue(조사·수정 대기열)와
sp_update_proposals(구체적 패치 초안 대기열). 어느 쪽이든
자동으로 approved가 되는 경로는 존재하지 않는다
(RULE-03과 동일 원칙).
stateDiagram-v2
[*] --> queued : ①/② 자동 큐잉
[*] --> pending_review : ③ 자동 초안 생성
queued --> in_review : 사람이 조사 착수
in_review --> resolved : 사람이 SP 직접 수정·커밋
pending_review --> approved : 사람이 승인
pending_review --> rejected : 사람이 기각
approved --> [*] : 다음 SP 버전에 반영
rejected --> [*]
resolved --> [*]
| 큐 | 자동이 넣는 상태 | 다음 단계 | 실제 반영 |
|---|---|---|---|
sp-author/queue |
queued (priority: low/high) |
사람이 조사·SP 원문 수정 | 사람이 커밋·PR |
sp_update_proposals |
pending_review |
사람이 초안 검토(수정/기각/승인) | 사람이 approved로 전환해야 다음 SP 버전에 반영 |
수동 트리거 (GitHub CLI)
정기 스케줄을 기다리지 않고 즉시 실행하거나, 실제 반영 없이 결과만 미리
보고 싶을 때 --dry-run(또는 워크플로 입력 dry_run=true)을 쓴다.
실행 상태 확인
STATUS 칸에 ✓가 뜨면 정상 종료다. *는 진행 중, X는
실패이니 --log-failed로 원인을 확인한다.
필요 시크릿·환경변수
| 변수 | 사용처 | 비고 |
|---|---|---|
POCKETBASE_URL | ③ 사용자 피드백 취합 | GitHub Secrets |
POCKETBASE_ADMIN_EMAIL | ③ 사용자 피드백 취합 | GitHub Secrets |
POCKETBASE_ADMIN_PASSWORD | ③ 사용자 피드백 취합 | GitHub Secrets |
HONDI_WORKER_URL | ③ 사용자 피드백 취합 | 워크플로에 하드코딩(공개 URL) |
HONDI_PROXY_URL | ① · ② (기본값 존재) | 공개 엔드포인트라 시크릿 불필요 |
①·②는 worker.js의 /sp-author/*·/gwp-registry/*가
인증 없는 내부 API라 별도 시크릿이 없다 — 실서비스 확장 시 인증 도입
여지는 있으나 현재 범위 밖이다. ③만 L1 PocketBase 관리자 인증이 필요하다.
결과물 확인 위치
data/feedback_triage_report.json— ③의 클러스터별 요약(신규 피드백이 없으면 파일 자체가 생성되지 않고 정상 종료된다, 아래 장애 이력 참조)- PocketBase Admin UI의
sp_author_queue/sp_update_proposals컬렉션 — ①·②·③ 모두의 최종 검토 대기열
triage_feedback.py는 처리할 신규 피드백이 없으면(초기 단계처럼
데이터가 아직 안 쌓인 경우 포함) data/feedback_triage_report.json
자체를 만들지 않고 조용히 종료하도록 설계돼 있다(정상 동작). 하지만
워크플로의 커밋 스텝은 그 파일이 무조건 있다고 가정하고
git add를 시도해 fatal: pathspec ... did not match any files로
실패했다. triage-user-feedback.yml의 커밋 스텝을
if [ -f data/feedback_triage_report.json ]; then ... else로
감싸 파일 존재 여부를 먼저 확인하도록 수정했다 — 스크립트 쪽 버그가
아니라 워크플로 쪽 가정 오류였다.
이 사례는 “정상적으로 아무 일도 안 하는 경우”를 워크플로가 실패로 오인하지 않도록, 조건부 스텝을 항상 명시적으로 짜야 한다는 일반 교훈으로 남는다 — 특히 초기 단계라 데이터가 적어 “처리할 것 없음”이 자주 발생하는 파이프라인일수록 그렇다.