데스크톱으로 돌아가기
AUTO_REFRESH_MANUAL v1.1

SP 스스로 낡지 않게 만드는 세 개의 자동 갱신 파이프라인

혼디의 SP(Service Persona)는 시간이 지나면 수치·법령·연락처가 바뀌어 낡는다. 사람이 수백 개의 SP를 매번 순회하며 낡은 곳을 찾는 건 비현실적이라, “무엇을 갱신해야 하는지 찾아서 큐에 올리는 일”만 자동화했다. 실제 문안 작성과 반영 승인은 이 문서 전체에서 예외 없이 사람(주피터님) 몫이다.

대상 저장소 Openhash-Gopang/gopang 실행 위치 GitHub Actions (저장소 밖 스케줄러 대체) 최종 갱신 2026-07-28
Overview
개요 — 왜 “자동 갱신”이 필요한가

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 원문 갱신")]
          
그림 1 — 세 파이프라인은 모두 큐잉까지만 자동화되고, 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 오류·개선 요청
Core Principle
공통 원칙 — 자동은 “발견”까지, 반영은 사람이

세 파이프라인 모두 동일한 경계선을 지킨다.

  • 자동으로 하는 것: 조회 · 계산 · 클러스터링 · 큐잉 · (③에 한해) 패치 초안 작성
  • 자동으로 하지 않는 것: SP 파일 직접 수정, sp_update_proposals 상태를 approved로 변경, 반영 배포
원칙
허위는 절대 안됩니다라는 혼디 전체 원칙과 맞닿아 있다 — 근거 부족한 상태에서 자동으로 “이게 맞다”고 SP를 고쳐버리면 그 오류가 실제 민원 응대에 그대로 흘러간다. 그래서 표본이 부족하거나(③의 최소 클러스터 크기 3건), 귀속이 모호하면(② 반복 3건 미만, ③ 대상 SP가 둘 이상) 아예 큐에 올리지 않고 리포트에만 남긴다 — “과신 방지”가 설계 전체를 관통한다.
Component 01
SP Refresh Scheduler — tier 재계산 + 갱신 큐잉
cron 0 0 * * * (매일 09:00 KST) 사람: 실제 조사·수정

실행 파일

  • 워크플로: .github/workflows/sp-refresh-scheduler.yml
  • 스크립트: tools/sp_refresh_scheduler.py

tier 배정 기준

gwp_registrycall_count_30d(최근 30일 호출 횟수)를 절대 임계값과 비교해 갱신 주기를 재배정한다 (실시간 퍼센타일이 아니라 docs/SP-AUTHOR-AUTOMATION_v1_0.md §2-2의 고정 수치를 그대로 쓴다 — 항목 수가 적을 때 퍼센타일은 쉽게 왜곡되기 때문).

tier기준(call_count_30d)재검토 주기예시
weekly200회 이상7일K-Health·K-Market·kgov/jeju 등 고빈도 서비스
monthly20~199회30일대부분의 K-서비스·행정시/읍면동 SP
quarterly20회 미만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(["사람 검토"])
          
그림 2 — tier 재계산부터 큐잉까지, 실제 수정은 항상 사람 검토 이후

큐잉 우선순위는 항상 low다 — 실사용자가 직접 신호를 보낸 경우(②·③)보다 후순위로 둔다는 것이 SP-AUTHOR-AUTOMATION_v1_0.md의 설계 원칙이다.

Component 02
미등록 태그 반복 집계 — 신규 SP 수요 발굴
sp-refresh-scheduler.yml에 통합 실행 사람: 신규 SP 실제 작성

실행 파일

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 작성"])
          
그림 3 — 동일 미등록 태그 반복 시 병합·승격·알림까지만, 신규 SP 본문은 사람이 작성

기본 임계값은 7일 · 3건이며, --days/--threshold 옵션으로 조정할 수 있다.

Component 03
사용자 피드백 취합 — 클러스터링 + 패치 초안
cron 0 22 * * 1 (매주 월 07:00 KST) 사람: 검토·승인만

실행 파일

  • 워크플로: .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 커밋
그림 4 — 클러스터링→브릿지 조건 검사→(충족 시) 초안 작성→검토 대기열, 조건 미충족은 리포트로만 남음
브릿지 조건기준이유
클러스터 크기≥ 3건표본 부족으로 인한 과신 방지
context_sp단일대상 SP가 모호하지 않아야 함
categorybug 또는 feature_request칭찬·단순 질문은 SP 수정 제안거리가 아님

초안 작성이 네트워크 오류 등으로 실패해도 조용히 플레이스홀더 문구로 폴백한다(confidence: low) — 초안 생성 실패가 클러스터링·리포트 생성 자체를 막으면 안 되기 때문이다. 초안이 실제로 만들어지면 confidence: medium으로 표시하지만, 이 값도 어디까지나 참고용이고 사람 검토를 대신하지 않는다.

실전 검증 전 주의
user_feedback 컬렉션 스키마는 worker.jshandleUserFeedbackSubmit() payload를 기준으로 추정했고, 클러스터링 임계값(0.82)도 실측 데이터 없이 잡은 잠정값이다. 실제 피드백이 쌓이면 재조정이 필요할 가능성이 높아, 매 실행마다 리포트에 클러스터 크기 분포를 남겨 조정 근거로 삼는다.
Human Review Gate
사람 승인 게이트 — 세 파이프라인의 공통 종착점

세 파이프라인이 만들어낸 신호는 결국 두 곳 중 하나로 모인다 — 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 --> [*]
          
그림 5 — 어느 큐에서 출발하든 사람의 결정 없이는 SP 원문에 닿지 않는다
자동이 넣는 상태다음 단계실제 반영
sp-author/queue queued (priority: low/high) 사람이 조사·SP 원문 수정 사람이 커밋·PR
sp_update_proposals pending_review 사람이 초안 검토(수정/기각/승인) 사람이 approved로 전환해야 다음 SP 버전에 반영
철학과의 관계
혼디는 안내가 아니라 실행이 주된 목적이라는 철학과, 이 게이트가 충돌하는 것처럼 보일 수 있지만 그렇지 않다 — “실행”은 사용자를 향한 서비스 응답에 적용되는 원칙이고, SP 자체를 고치는 일(플랫폼의 지식 원본을 바꾸는 일)은 항상 사람 승인을 거친다. 여기서 자동화가 대신하는 건 “무엇을 고쳐야 하는지 찾아내는 사람의 반복 노동”이지, “고칠지 말지 판단하는 권한”이 아니다.
Operations
운영 — 수동 실행 · 결과 확인 · 필요 환경변수

수동 트리거 (GitHub CLI)

정기 스케줄을 기다리지 않고 즉시 실행하거나, 실제 반영 없이 결과만 미리 보고 싶을 때 --dry-run(또는 워크플로 입력 dry_run=true)을 쓴다.

gh workflow run "Triage user feedback (cluster + draft patch proposals)" -f dry_run=true gh workflow run "SP refresh scheduler (Phase 4)" -f dry_run=true

실행 상태 확인

gh run list --workflow="triage-user-feedback.yml" --limit 1 gh run list --workflow="sp-refresh-scheduler.yml" --limit 1 # 실패 시 로그 확인 gh run view <RUN_ID> --log-failed

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 컬렉션 — ①·②·③ 모두의 최종 검토 대기열
Incident Log
장애 이력 — 실사로 발견·수정된 문제
2026-07-28
“Commit triage report” 스텝이 리포트 파일 부재를 처리하지 못함.
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로 감싸 파일 존재 여부를 먼저 확인하도록 수정했다 — 스크립트 쪽 버그가 아니라 워크플로 쪽 가정 오류였다.

이 사례는 “정상적으로 아무 일도 안 하는 경우”를 워크플로가 실패로 오인하지 않도록, 조건부 스텝을 항상 명시적으로 짜야 한다는 일반 교훈으로 남는다 — 특히 초기 단계라 데이터가 적어 “처리할 것 없음”이 자주 발생하는 파이프라인일수록 그렇다.