尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Bullet-in: 아스날 FC 뉴스를 3시간마다 수집하여 (영국 현지 언론, ITK) 한국어로 번역 · 요약하고 신뢰도순으로 보여 주는 서비스 및 데이터 파이프라인
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
영국 현지 언론과 ITK (X) 에 흩어진 Arsenal FC 소식을 하루 8회 병렬 수집하고 공신력 스코어링과 중복 제거를 거쳐 LLM 으로 번역 · 요약한 뒤 신뢰도순으로 제공하는 뉴스 데이터 파이프라인입니다. 공개 서비스: https://bullet-in.pages.dev · 2026-08-29 공개 · Airflow 가 3시간마다 파이프라인을 실행해 수집하고 검증하고 배포합니다. 현재 가동 상태는 수집 현황 대시보드 의 페이지 생성 시각과 SLO 표에서 확인할 수 있습니다. Bullet-in = bulletin (단신) + bullet (병기고 Arsenal) 의 언어유희입니다. 방문자가 처음 보는 홈 화면입니다. 대표 기사와 주요 소식을 신문 레이아웃으로 배치하고, 그 아래에 최신 뉴스와 가십을 나눠 싣습니다. 제목은 모두 원문을 한국어로 옮긴 것이며 매체명과 공신력 등급을 함께 표시합니다. 기사 상세 화면입니다. 3줄 요약, 한국어로 옮긴 본문, 원문 링크, 기자 바이라인을 표시합니다. 위 수치는 수집 현황 대시보드 가 실행할 때마다 새로 계산합니다. 설계 판단만 빠르게 확인하려면 8. 설계 결정과 트레이드오프 로 바로 이동해도 됩니다.檢視完整 README ↗
영국 현지 언론과 ITK (X) 에 흩어진 Arsenal FC 소식을 하루 8회 병렬 수집하고 공신력 스코어링과 중복 제거를 거쳐 LLM 으로 번역 · 요약한 뒤 신뢰도순으로 제공하는 뉴스 데이터 파이프라인입니다.
공개 서비스: https://bullet-in.pages.dev · 2026-08-29 공개 · Airflow 가 3시간마다 파이프라인을 실행해 수집하고 검증하고 배포합니다. 현재 가동 상태는 수집 현황 대시보드 의 페이지 생성 시각과 SLO 표에서 확인할 수 있습니다.
Bullet-in = bulletin (단신) + bullet (병기고 Arsenal) 의 언어유희입니다.
방문자가 처음 보는 홈 화면입니다. 대표 기사와 주요 소식을 신문 레이아웃으로 배치하고, 그 아래에 이적 뉴스 · 선수 · 팀 뉴스 · 가십을 나눠 싣습니다. 제목은 모두 원문을 한국어로 옮긴 것이며 매체명과 공신력 등급을 함께 표시합니다.
기사 상세 화면입니다. 3줄 요약, 한국어로 옮긴 본문, 원문 링크, 기자 바이라인을 표시합니다.
| 항목 | 내용 |
|---|---|
| 서비스 | 2026-08-29 공개 · 공개일 688명 · 첫 주 827명 |
| 규모 | 활성 소스 9종 (그중 수집 8종) · 기사 1,302건 (2026-10-07 기준) · 3시간 간격 실행 |
| 신뢰성 | 완주율 99.4% (2026-07-20 이후 628회 중 624회 · 2026-10-07 기준) · 중복 적재율 0% · 필수 필드 완전성 100% |
| 품질 게이트 | 실행 마지막 단계의 dbt 데이터 계약 테스트 22종 · 하나라도 실패하면 배포 중단과 Discord 알림 |
| 배포 | 머지된 코드를 다음 실행이 자동으로 내려받아 배포 · 운영 환경에서 반영 검증 · 불일치 시 이전 커밋으로 롤백 |
| 복구 | 매일 GCS 백업 · 별도 DB 에 복원해 테이블 6개와 raw_items 의 행 수를 백업 매니페스트와 대조 (2026-09-01 복구 테스트 전부 일치) |
| 비용 | Iceberg 테이블은 GCS · 카탈로그만 Google Lakehouse runtime catalog · 매니지드 Iceberg 테이블의 시간당 요금 없이 GCS 저장 · 작업 요금만 발생 (금액은 GCP 결제 보고서에서 조회) |
위 수치는 수집 현황 대시보드 가 실행할 때마다 새로 계산합니다. 설계 판단만 빠르게 확인하려면 8. 설계 결정과 트레이드오프 로 바로 이동해도 됩니다.
이 문서 읽는 법
아스날 관련 소식을 확인하려면 영국 언론 5~6곳과 ITK (In The Know) 트위터 계정을 각각 방문해야 합니다. 매체와 계정마다 공신력 편차가 커서, 같은 이적설이라도 어디서 나왔는지에 따라 신뢰도가 크게 달라집니다. 이 불편을 해결하기 위해 신뢰할 수 있는 소스만 선별해 한곳에 모으고, 한국어로 번역 · 요약해 신뢰도순으로 제공하는 서비스를 만들었습니다.
단순 수집 스크립트가 아니라 신뢰성 · 멱등성 · 데이터 품질 · 관측성을 갖춘 데이터 프로덕트로 설계했습니다. 공개일에 688명, 공개 첫 주 (08-29 ~ 09-04) 에 827명이 방문했습니다 (순 사용자 기준이며, 광고 차단 환경의 방문은 집계되지 않습니다).
메달리온 아키텍처 (Bronze → Silver → Gold) 위에 LLM 번역 · 요약, 데이터 품질 게이트, 배포 자동화를 올린 구조입니다. 파이프라인 1회 실행은 Airflow DAG 의 태스크 8개로 나뉩니다.
왼쪽이 입력 (수집 소스 · 코드 저장소 · GA4 사이트 태그), 가운데가 Oracle Cloud VM 에서 도는 수집 · 저장과 Airflow DAG, 아래가 Google Cloud 의 레이크하우스, 오른쪽이 서빙과 알림입니다. 글자가 작으면 그림을 누르면 3배 크기로 열립니다. 그림의 「소스 10종」 은 설정에 등재된 수를 말하며 이 가운데 9종이 활성이고 8종에서 수집합니다 (§3).
DAG 안에서 태스크는 이 순서로 돕니다.
advance -> collect -> enrich -> publish -> gate -> deploy_site -> judge
|
+-> warehouse_load (publish 이후 병렬)
| 태스크 | 하는 일 |
|---|---|
advance | origin/main 내려받기 (사람이 운영 서버에서 pull 하지 않는 구조) |
collect | 수집 소스 8종을 어댑터 5종으로 asyncio 병렬 수집 → 정규화 → URL · content_hash 기준 중복 제거 → 공신력 tier 산출 → Bronze · Silver 적재 |
enrich | Gemini API 로 번역 · 요약 · 영입 단계 분류 (신규 행만 처리하는 멱등 설계) |
publish | 정적 HTML 렌더 (기사 · 선수 · 대시보드 2종) · 실행 기록 · 신선도 판정 |
gate | dbt build 와 테스트 22종 (DuckDB 가 MariaDB 를 attach) · 실패 시 배포 중단 |
deploy_site | Cloudflare Pages 업로드 (산출물이 비정상이면 중단) |
judge | 라이브 build.json 으로 반영 확인 · 불일치 시 이전 커밋으로 롤백 · Discord 알림 |
warehouse_load | MariaDB 변경분 · 스냅샷과 GA4 행동 로그를 Iceberg (GCS) 에 적재 · Gold 재작성 |
실행 주기는 3시간이고 실행기는 LocalExecutor 입니다 (§5).
systemd 는 파이프라인 외부의 부가 작업만 담당합니다: 선수 워치리스트 (귀속된 선수를 fmkorea 에서 재검색) · 일일 백업 (GCS) · 레이크하우스 유지보수 (컴팩션 · 스냅샷 만료 · 고아 데이터 파일 청소) · Airflow 감시 (하트비트 · 실행 지연).
httpx, RSS = BBC Sport, 구단 공홈 = 사이트맵으로 후보를 찾고 GraphQL 로 본문 조회, X = 쿠키 주입 Playwright).asyncio 팬아웃으로 소스를 동시에 호출하며, 한 소스가 실패해도 나머지 수집은 정상 진행됩니다.content_hash 와 URL 정규화에 DB UNIQUE 제약을 더해 애플리케이션과 DB 양쪽에서 중복을 막습니다.confidence 값으로 정렬합니다.
기자 단위 tier 가 매체 tier 보다 우선 적용됩니다 (전담 기자의 기사만 등급 상향).전체 기사 화면 (
all.html) 입니다. 날짜별 시간순으로 정렬하고, 왼쪽 레일에서 뉴스 종류 · 공신력 · 소스 · 기자 · 영입 단계로 범위를 좁힐 수 있습니다. facet 옆의 숫자는 현재 적재된 기사가 각 갈래에 몇 건씩 있는지 보여 줍니다.
활성 소스는 9종이며 고정 tier 7종과 항목별 동적 tier 2종으로 구성했습니다 (X 와 커뮤니티는 게시물에 언급된 기자 · 매체의 공신력으로 tier 를 산출합니다).
아래 표에는 2026-07 에 비활성으로 돌린 1종 (football.london) 과 2026-08-15 에 수집을 멈춘 1종 (Goal.com) 을 포함해 10종을 싣습니다.
언론 5종은 공통 이적 키워드 필터를 공유합니다 (config/sources.yaml).
그중 수집 중인 3종 (BBC Sport · Sky Sports · The Guardian) 은 명단의 선수 · 감독 이름과 팀 낱말로 선수 · 팀 소식과 경기 평점까지 받습니다 (2026-10 개정).
| 소스 | tier | 어댑터 | 비고 |
|---|---|---|---|
| Arsenal.com | 0 | arsenal_api | 공식: 공홈 사이트맵으로 후보 발견 + GraphQL 로 본문 조회, taxonomy 필터 (이적 · 1군 재계약) · 2026-10 부터 1군 선수 · 팀 소식 (대표팀 · 수상 · 인터뷰) |
| BBC Sport | 1.5 | rss | 아스날 RSS (2026-10 팀 페이지에서 전환) · 비전담 기준선: 전담 기자 (Mokbel) 는 tier 1 로 상향 |
| Sky Sports | 2 | html | 비전담 기준선: 전담 기자 (Sheth) 는 tier 1.5 로 상향 |
| The Guardian | 3 | html | 2026-07 에 Open Platform API 에서 전환 (무료 등급 API 가 독점 기사를 제외) |
| Goal.com | 4 | html | 2026-08-15 수집 중단 (다른 소스 기사와 72% 중복 · collect: false) · 이미 적재된 기사는 그대로 서빙 |
| BBC Football Gossip | 4 | html | 타 매체 루머 라운드업 |
| football.london | 4 | html | 2026-07 이후 비활성 (enabled: false) · 과거 실행 기록에는 남아 있음 |
| afcstuff (X) | 동적 | x_playwright | 트윗에 언급된 기자 · 매체 tier 로 라우팅 (fallback 4) |
| David Ornstein (X) | 1 | x_playwright | 기자 본인 계정: 트윗에 @핸들이 없어 고정 tier |
| fmkorea 축구 소식통 | 동적 | fmkorea | 한국 커뮤니티: 언급된 기자 · 매체 tier 로 라우팅 (기본 4) |
기자 · ITK 공신력: 동적 소스는 항목마다 tier 를 개별 산출합니다. 기자를 먼저 조회하고, 없으면 매체를 조회하고, 둘 다 없으면 기본값을 적용합니다.
항목 1건의 tier 산출
1. 게시물에 언급된 기자가 레지스트리에 있는가?
있으면 -> 기자 tier (전담 기자는 매체보다 높다)
2. 없으면, 해당 매체가 레지스트리에 있는가?
있으면 -> 매체 tier
3. 기자도 매체도 레지스트리에 없으면
기본값 -> tier 4 (출처를 특정하지 못한 루머)
레지스트리 전체 (기자 · ITK 105명 · 매체 55곳 · 별칭) 는 config/credibility.yaml 에 있으며 화면의 기자 필터도 같은 파일을 참조합니다.
목표치와 측정 방법입니다. 번호는 수집 현황 대시보드 의 SLO 표와 동일하며 SLO-2 부터 6 까지는 실행마다 해당 대시보드에 갱신됩니다. 병렬화 측정 절차는 SLO-1 벤치마크 런북, 지표 정의는 SLO 측정 런북 에 정리했습니다.
| 번호 | 지표 | 목표 | 측정 방법 | 실측 (2026-10-07) |
|---|---|---|---|---|
| SLO-1 | 병렬화 수집 시간 단축 | 순차 대비 55% 이상 단축 (실측 기반 재조정¹) | metrics.benchmark() (concurrency=1 vs N 벤치마크) | 56.5% 단축 (2026-07-15 · 3회 중앙값 · 실행마다 측정하지 않음) |
| SLO-2 | 실행 성공률 | 99% 이상 | pipeline_runs.success_rate 최근 30회 평균 (재시도 · 소스 격리 포함) | 99.2% (최근 30회 중 2회 아스날 공식 소스 사이트맵 응답 타임아웃 · 사이트맵 재시도로 대응) |
| SLO-3 | 중복 적재율 | 0% | content_hash · URL UNIQUE 제약 + dbt unique 테스트 5종 | 0% (기사 1,302건) |
| SLO-4 | 필수 필드 완전성 | 99% 이상 | dbt not_null 테스트 10종 | 100% |
| SLO-5 | 소스 신선도 | 끊긴 소스 0 | 목록 페이지 응답 여부 (2회 연속 무응답) · 목록 변경 여부 (48시간 초과 미변경) 로 판정한 source_freshness.state | 0 (BBC Sport 를 아스날 RSS 로 전환해 끊김 해소) |
| SLO-6 | 수집량 이상 감지 | 이상 소스 0 · ±2σ 알림 | quality.volume_anomalies (직전 실행 대비) | 0 · 가동 중 (실발송 검증 2026-07-13) |
¹ 초기 목표 70% 를 도달 불가로 판정하고 55% 로 하향한 경위는 8. 설계 결정과 트레이드오프 에 있습니다.
실행 · 배포 · 감시를 모두 자동화했습니다. 2026-09-04 에 systemd 타이머에서 Airflow DAG 로 마이그레이션했습니다. 첫 24시간 동안 정규 실행 8회가 모두 성공했습니다 (소요 3.5~6.0분 · 재시도 0 · 오탐 0 · 런북 §6.5).
LocalExecutor · Postgres 메타 DB · DAG 1개 · 태스크 8개 (§2)catchup=False: 밀린 실행은 1회만 수행max_active_runs=1: 동시 실행 금지dagrun_timeout 30분advance 가 origin/main 을 내려받고, 파이프라인 실행 후 judge 가 라이브의 build.json 으로 반영 여부를 확인합니다.
설계는 배포 자동화 스펙 에 있습니다.
gate (dbt 테스트 22종)
|
+-- 통과
| +-- deploy_site -> judge 가 라이브 build.json 의 커밋 해시를 대조
| |
| +-- 일치 -> 반영 완료 + 리뷰 채널 알림
| +-- 불일치 -> 이전 커밋으로 롤백 + 사고 채널 알림
|
+-- 차단
+-- deploy_site 를 건너뛴다 (배포 없음) + 사고 채널 알림
게이트 결과 파일이 없으면 통과로 간주하지 않고 차단으로 판정합니다 (8. 설계 결정과 트레이드오프).
Discord 채널 2개 (사고 · 리뷰) 를 운영합니다. 모든 알림에 「무엇이 · 어디서 · 다음에 확인할 로그」 를 함께 담습니다.
raw_items 의 행 수를 백업 매니페스트와 대조
원본을 Bronze 에 불변으로 보존하므로 판정 규칙이 바뀌면 전체 재처리가 가능합니다.
src/bullet_in/ 의 backfill_*.py 15개와 migrate_*.py 2개
schema.sql 의 ALTER TABLE … IF NOT EXISTS 30건으로 멱등하게 처리
ensure_schema() 가 다시 적용합니다.정적 HTML 로 실행할 때마다 새로 생성하며 검색 엔진에는 노출하지 않습니다.
행동 지표: DAU · 퍼널 (진입 → 카드 클릭 → 반복 → 재방문) · 요일 × 시각 히트맵 · 관심 지수 · 리텐션 · 화면별 클릭 · 페이지 · 상위 기사 · 선수 페이지를 보여 줍니다. GA4 이벤트를 Iceberg Gold 테이블로 집계해 렌더링합니다.
행동 로그의 출처 (GA4 → BigQuery → Iceberg)
행동 지표 대시보드의 수치는 GA4 가 BigQuery 로 매일 내보낸 이벤트 원본을 warehouse_load 태스크가 Iceberg Bronze 로 적재해 집계한 결과입니다.
bi_entry (유입) · bi_card_click (카드 클릭) · bi_filter_apply (필터) · bi_origin_exit (원문 이탈)
src/bullet_in/serve/static/app.js 에 있습니다.bi_card_click 의 매개변수 card_hash 가 기사의 content_hash 와 같습니다.
수집 현황: SLO 6개 행 · 완주율 타일 · 일별 신규 · 실행 수 캘린더 · 소스 × 주차 · 처리량 · 소요 시간 분포 · 발행에서 수집까지의 지연 · 선수 축 · 공신력 · 단계 구성 · 소스 신선도를 한 화면에 모았습니다.
수집 현황의 출처 (MariaDB Silver → 화면)
행동 지표와 달리 별도의 집계 저장소를 두지 않고 서빙 DB 를 직접 읽습니다. 완주율 타일만 예외로, 감시 타이머가 남기는 상태 파일을 읽습니다.
pipeline_runs: 실행마다 한 행 · 신규 · 중복 · 에러 · 소요 시간 · 소스별 건수articles: 등급 · 이적 단계 · 발행 시각source_freshness: 실행 × 소스의 마지막 수집 시각과 임계unique · not_null 테스트 결과 (SLO-3 과 SLO-4 의 값)state/completion.json): 매시 감시 타이머가 Airflow 실행 목록과 이전 systemd 실행 기록에서 센 완주 · 시작 수 (「완주율」 타일의 값)화면 맨 위의 「데이터 원천」 절이 앞의 네 가지를 밝히고 있어 열어서 대조할 수 있습니다 (완주율 상태 파일은 그 절에 없습니다).
데이터 계약을 코드로 선언해 두고, 통과하지 못하면 배포를 중단합니다.
gate 태스크가 dbt build 로 스테이징 5개와 Gold 3개를 생성하고 테스트 22종을 돌립니다.
unique 5 · not_null 10 · accepted_values 5 · relationships 2deploy_site 가 실행되지 않고 알림이 발송됩니다.LLM 이 만든 산출물을 LLM 없이 규칙 코드로 검사합니다. 경로는 둘이고, 경로마다 검사 항목과 다시 생성하는 방식이 다릅니다.
| 경로 | 대상 | 검사 | 걸렸을 때 |
|---|---|---|---|
| 번역 | 영문 원문을 번역하는 기사 | 번역 제목에 들어간, 원문에 없는 인명 · 제목 오역 (인명 누락 · 근거 없는 「임대」) · 라운드업 단신 누락 · 원문에 없는 구단명 | 다음 실행에서 사유를 프롬프트에 붙여 한 번 다시 번역합니다. 그래도 걸리면 번역 결과를 채택하고 경고를 남깁니다. |
| 게시글 재작성 | 이미 한국어인 커뮤니티 게시글 본문 | 숫자 누락 · 수치 주입 · 인용 보존 · 구단명 주입 · 인명 주입 · 원문 복제 (글자 8-gram 잔존율이 0.75 초과) | 같은 실행 안에서 사유를 붙여 최대 두 번 다시 생성하고, 세 시도 중 위반이 가장 적은 것을 채택합니다. 위반이 남으면 다음 실행에서 한 번 더 생성합니다. |
게이트는 재생성 트리거이지 폐기 조건이 아니라서 본문을 버리지 않습니다. 재작성 잔존율이 임계를 넘은 채로 남은 기사는 수집 현황 대시보드의 「확인 대상」 표에 올라 사람이 검토합니다.
| 영역 | 선택 | 이유 |
|---|---|---|
| 서빙 mart | MariaDB | 일 수십 건 규모의 서빙 (포인트 조회 · 필터 · UNIQUE dedup) 에는 OLTP 가 적합 |
| 원본 랜딩 | MongoDB | 구조가 제각각인 원문을 손실 없이 schema-on-read 로 보존해 언제든 재처리 가능 |
| 품질 · 분석 | dbt + DuckDB | dbt test 가 데이터 계약 검증과 그대로 대응 · DuckDB 가 MariaDB 를 attach 해 별도 인프라 없이 집계와 테스트 수행 |
| 레이크하우스 | Apache Iceberg on GCS + Google Lakehouse runtime catalog | 변경 이력 · 스냅샷 · 행동 로그처럼 append 위주 데이터를 서빙 DB 외부에 적재 · PyIceberg 로 직접 쓰고 카탈로그만 매니지드 서비스를 사용해 운영할 서버가 없음 (§8) |
| 스크래핑 | Playwright / httpx | 소스 난이도 (정적 · 쿠키 인증 · 안티봇) 에 맞춰 도구를 선택 · X 는 쿠키 주입 Playwright |
| 스케줄 · 배포 | Airflow 3 (LocalExecutor) + wrangler | 실행을 태스크 8개로 분리해 3시간마다 수행 · 실패한 태스크만 식별되고 판정 태스크가 배포를 롤백 (§8) · 실행 종료 시 Pages 직접 업로드 |
| LLM 번역 · 요약 | Gemini 3.1 Flash-Lite | 새 기사 하루 15 ~ 17건 규모 (이적 기간) · 단순 번역에 맞는 단가 · response_mime_type 으로 JSON 출력 강제. 유료 (Tier 1 선불) 이며 월 요금은 GCP 결제 보고서에서 조회 (문서에 금액을 고정하면 값이 낡음) |
선택한 것보다 선택하지 않은 것과 그에 따라 감수한 것을 기록합니다.
CDC 를 사용하지 않았습니다
content_hash 비교 + revision 증가) 를 사용했습니다.articles_changes 에 적재합니다.SLO-1 목표를 70% 에서 55% 로 하향했습니다
매니지드 Iceberg 테이블을 사용하지 않았습니다
BigLake Table Management 는 관리 작업이 쓴 연산량에 시간당 165.99 KRW 를 과금하고 무료 구간이 없습니다. 한 달 내내 돈다고 가정하면 12만원이 넘습니다 (레이크하우스 설계 §2).품질 게이트와 웨어하우스 적재를 분리했습니다
게이트 실패 시 배포를 차단하는 쪽으로 설계했습니다 (fail-closed)
메달리온을 2개 운영합니다. 기사와 행동 로그는 원천 시스템도 저장소도 달라 층을 따로 구성했으며, 둘 다 같은 DAG 가 채웁니다.
메달리온 1 · 기사
Bronze · MongoDB
- raw_items · 소스 원문을 변형 없이 보존
- 신선도 판정의 워터마크가 이 층에서 나옵니다
|
v 정규화 · content_hash 계산 · 공신력 tier 산출 · UNIQUE 중복 제거
Silver · MariaDB
- 테이블 6개 · 서비스 화면과 수집 현황 대시보드가 직접 조회합니다
- articles · sources · players · article_players
- pipeline_runs · source_freshness
|
v dbt build (실행의 마지막 단계)
Gold · dbt + DuckDB
- 집계 모델 3개 · 테스트 22종이 배포를 막는 품질 게이트
Silver 는 mart_history (Iceberg) 로도 흘러갑니다 · 아래 레이크하우스 절
메달리온 2 · 행동 로그 Iceberg on GCS 의 behavior 네임스페이스
Bronze · ga4_events
- BigQuery 일별 내보내기 · 중첩 · 반복 필드를 그대로 적재
|
v Flatten (ARRAY · STRUCT 해제) · 중복 이벤트 제거
Silver · ga4_events_flat
- 1행 1이벤트 · 1열 1값
|
v 전량 재구축 (신규 · 재방문은 전체 이력이 필요한 비가산 지표)
Gold
- 팩트 3개 · fact_card_click · fact_session · fact_user_daily
- 디멘션 2개 · dim_date · dim_user
층 이름은 실제 데이터가 존재하는 자리에만 붙였습니다.
raw_items): 원문을 변형 없이 보존합니다.
신선도 판정의 워터마크가 이 층에서 산출됩니다.articles: 정규화 메타 + tier + confidence + 번역 · 요약 · content_hash 와 url 에 UNIQUE 로 dedupsources · players: 소스와 선수 명단article_players: 기사 × 선수 매핑 (주체 · 언급)pipeline_runs: 실행별 SLO 근거source_freshness: 실행 × 소스 신선도 이력models/gold/ 모델 3개): gold_daily_source_quality · gold_slo_rollup · gold_tier_distribution.
실행 종료 시 dbt build 가 갱신하며 같은 실행의 테스트 22종이 품질 게이트로 동작합니다.models/staging/ 의 모델 5개는 MariaDB 테이블을 그대로 읽어 오는 경유 뷰라 층 이름을 붙이지 않았습니다.
레이크하우스 (Iceberg on GCS): 서빙 DB 외부에 적재하는 네임스페이스 2개입니다.
mart_history: 서빙 DB 가 덮어쓰면 사라질 값을 남깁니다.
articles_changes: 실행별 변경분articles_snapshot · players_snapshot · article_players_snapshot: 90일까지 매일 · 이후 주 1회ops_daily: 일별 운영 지표behavior: GA4 이벤트 (BigQuery 일별 내보내기 · §5) 를 층으로 나눠 적재합니다.
ga4_events · Silver ga4_events_flatfact_card_click · fact_session · fact_user_dailydim_date · dim_useruser_pseudo_id 만 씁니다 (사용자 키를 혼용하면 방문자가 두 배로 집계된다).| 경로 | 내용 |
|---|---|
src/bullet_in/run.py · pipeline.py | 실행 1회의 진입점과 항목 판정 (여성 축구 제외 · 본문 등급 · 기자 선택) |
src/bullet_in/adapters/ | 소스별 수집기 · 설정이 쓰는 5종 (arsenal_api · rss · html · x_playwright · fmkorea) · 코드만 있고 쓰는 소스가 없는 2종 (playwright_news · guardian_api) |
src/bullet_in/ingest.py · canonical.py · dedup.py | 병렬 수집 · URL 정본화와 content_hash · 신규 · 변경 · 중복 분류 |
src/bullet_in/credibility.py · score.py | 기자 · 매체 레지스트리 조회와 tier · confidence 산출 |
src/bullet_in/enrich.py · fidelity.py | LLM 번역 · 요약과 번역 품질 게이트 (§6) |
src/bullet_in/roster.py · transfer_stage.py | 선수 명단과 이적 단계 |
src/bullet_in/storage/ | MongoDB (Bronze) · MariaDB (Silver) 접근과 schema.sql |
src/bullet_in/serve/ | 정적 HTML 렌더 · 대시보드 2종의 뷰 · 차트 |
src/bullet_in/dbt_gate.py · deploy.py · warehouse.py | 품질 게이트 · 배포 판정과 롤백 · Iceberg 적재 |
src/bullet_in/quality.py · metrics.py · notify.py | 수집량 이상 감지 · SLO 측정 · Discord 알림 |
airflow/dags/ | DAG 정의와 태스크 스크립트 |
dbt/models/ | staging 5개 · gold 3개와 테스트 22종 |
config/ | 수집 소스 · 공신력 레지스트리 · 표기 사전 등 YAML 5개 |
tests/ | 단위 테스트와 integration/ (MariaDB 컨테이너가 필요합니다) |
infra/ | systemd 유닛 · 백업 · 배포 스크립트 |
docs/ | 설계 · 계획 · 런북 · 트러블슈팅 (§12) |
# 0. 환경
cp .env.example .env # 값 채우기 (Mongo · MariaDB · Gemini · X)
uv sync --extra dev
uv run playwright install chromium
# 1. 데이터 스토어
docker compose up -d # mongo, mariadb
# 2. 파이프라인 1회 실행 (dotenv 미사용 → 셸 export 필요)
set -a; source .env; set +a
uv run python -m bullet_in.run --concurrency 8 # collect → enrich → publish → gate 를 한 프로세스로
# 3. 결과 확인
open site/index.html # 기사 · 선수 · 대시보드 2종 (site/behavior.html · site/ops.html)
테스트는 uv run pytest -q 로 실행합니다 (통합 테스트는 MariaDB 컨테이너가 없으면 skip 됩니다).
Airflow DAG 임포트는 별도 venv 에서 검증합니다 (docs/MIGRATION.md).
운영 VM 의 실행 · 수동 트리거 · 롤백 절차는 Airflow 런북 에 있습니다.
설계 (docs/superpowers/specs/ 75편) · 계획 (docs/superpowers/plans/ 64편) · 런북 (docs/runbook/ 89편) · 트러블슈팅 (docs/troubleshooting/ 189편 · 폴더 안내문 README.md 제외) 이 있습니다 (2026-10-07 기준).
아래 5편을 먼저 읽는 것을 권장합니다.
트러블슈팅 문서는 「무엇이 틀렸는가」 보다 「해당 결함을 어떤 검증 수단이 놓쳤는가」 에 무게를 둡니다. 같은 검증 공백이 다른 곳에서 반복되기 때문입니다.
이 저장소는 한 사람이 Claude Code 와 함께 개발하고 운영합니다. 어디까지 AI 에 맡기고 어디부터 사람이 결정하는지, AI 가 만든 오류를 어떻게 걸러냈는지를 적었습니다.
| 순서 | 단계 | 주체 |
|---|---|---|
| 1 | brainstorming | 사람이 승인 |
| 2 | 스펙 | AI 초안 · 사람이 승인 |
| 3 | 계획서 (dry run) | AI 초안 · 사람이 승인 |
| 4 | Task 단위 구현 (subagent · TDD) | AI |
| 5 | spec 리뷰 · code 리뷰 | 리뷰 모델 |
| 6 | 실측 검증 | 사람 · 리뷰 모델 |
| 7 | PR 작성 | AI |
| 8 | Merge | 사람 |
| 9 | 배포 | 다음 파이프라인 실행이 수행 |
superpowers 스킬 묶음으로 고정합니다.
brainstorming: 아직 정해지지 않은 사항을 먼저 확인합니다.writing-plans: 스펙과 계획서를 문서로 만듭니다.subagent-driven-development: 계획서를 Task 단위로 구현합니다.verification-before-completion: 실행 결과를 근거로만 완료를 선언하도록 강제합니다.CLAUDE.md 에 병합했고 모든 코드 태스크에 동일하게 적용합니다.| 주체 | 맡은 일 |
|---|---|
| 사람 | 요구사항과 지표를 정의하고, 스펙 · 계획서 · 화면 목업을 승인하거나 반려합니다. 실측값을 화면 · DB · 로그로 대조하고, LLM 호출과 운영 데이터 정정을 승인합니다. 리뷰 피드백의 채택과 기각, 최종 전체 리뷰를 담당합니다. PR Merge 527건 (2026-10-07 기준) 은 전부 사람이 수행했습니다. |
| AI | 스펙과 계획서 초안, 코드와 테스트 (버그는 재현 테스트부터 작성), 실측 스크립트, 실데이터 기반 대시보드 목업, 문서 초안, 리뷰를 담당합니다. |
| AI 에 부여하지 않은 권한 | Merge 권한, 운영 서버에서의 git pull, 승인 없는 운영 데이터 정정입니다. 배포는 파이프라인의 첫 태스크가 origin/main 을 내려받는 방식이므로 사람도 서버에서 직접 pull 하지 않습니다. |
반려 예시: 잘린 트윗의 뒷부분을 프롬프트로 보완하자는 제안은 기각하고 수집기를 수정해 전문을 받도록 했습니다.
사람이 매번 확인하지 않아도 자동으로 걸러지도록 만든 장치입니다.
.claude/hooks/check-doc-format.py 가 커밋 · PR 컨벤션 §2.2 를 검사하고, CI 의 docs-format 잡이 변경된 docs/*.md 에 같은 검사를 한 번 더 돌립니다..claude/tools/check-pr-format.py 를 통과시킵니다.
동일한 위반이 5회 재발한 뒤에 만든 도구라 파일 상단에 그 이력을 기록해 두었습니다.동일한 실패가 반복되면 트러블슈팅으로 기록하고, 재발 방지 규칙을 CLAUDE.md 나 런북으로 옮기고, 검사 장치가 그 규칙을 받아 강화됩니다.
이 순환이 실제로 동작한 사례 4건입니다.
나머지는 docs/troubleshooting/ 에서 subagent · session · plan · prompt 로 검색하면 됩니다.
fact_user_daily · 08-29 ~ 09-04).
구독이나 알림 기능은 아직 없습니다..env 로 분리해 커밋하지 않습니다.
개인 학습 용도입니다.config/sources.yaml 의 serving: full · 지금 수집하는 소스는 8종).
번역 전문은 출처 표기 · 링크 · 비영리만으로 면책되지 않는다는 위험을 알고 받아들인 결정이며, 소스 단위로 요약과 앞부분 발췌만 싣도록 되돌리는 설정 (serving: excerpt) 이 준비돼 있습니다 (결정 기록).LLM 429 응답에 행 단위 백오프를 두지 않았습니다
systemd 타이머에서 Airflow 로 마이그레이션했습니다 (2026-09-04)