articles 스키마 AS-IS·TO-BE

bizcrush-pipeline · 개발 회의 참고자료

매거진 articles 스키마 — AS-IS 실측과 TO-BE 확장 지점

실측 2026-09-02 대상 Cloud SQL vertical-news-engine · magazine DB 경로 Auth Proxy 7231 · 읽기 전용

요지 — 재구성·digest·다국어(en, 향후 ja·zh)·원문 응답 전부 단일 articles 테이블로 대응 가능하다. 테이블 분리·대규모 마이그레이션이 필요한 지점은 없고, 스키마에서 실제로 걸리는 곳은 이다: ① 언어가 컬럼명에 박힌 fast_track_en_correction_id(translated_from_article_id, lang) 유니크 인덱스 부재.

AS-IS · 1재구성과 digest는 같은 스키마다

digest는 별도 테이블도, 별도 writer_mode 값도 아니다. CHECK 제약이 writer_mode IN ('original','reconstruct') 둘만 허용하고, 라이브 735행 전부 reconstruct다. digest 기사의 정체는 reconstruct 행 중 digest jsonb 컬럼이 채워진 것(74행 · 전부 ko · 인공지능신문 트랙)이다.

구분실측값
테이블public.articles — 29컬럼 단일 테이블 · 총 735행 (ko 469 · en 266)
digest jsonb 구조{ perspective: string, source: { url, author, publisher, published_at } }
digest 채움74행 / 735 — 전부 lang='ko' · en 형제 0건 (aitimes는 translate_to 없음)
API 노출/v1/publisheddigest·perspective_notice는 DB 컬럼이 아니라 이 jsonb에서 API 레이어 파생 (apps/api/engine_api/attribution.py)

AS-IS · 2컬럼 사전 — 역할 그룹별 29컬럼

공통 코어 위에 digest·번역·패스트트랙이 각각 nullable 컬럼 그룹으로 얹힌 구조다. 행의 성격(재구성/digest/번역본)은 그룹의 채움 여부로 갈린다.

그룹컬럼비고
공통 코어 id · cluster_id · lang · market · slug · title · body_md · refs · target_keywords · writer_mode · status · regen_count · review_note · published_at · created_at · updated_at · generated_title · summary · verify_detail · batch_stats · published_images refs=원문 URL 배열(jsonb) · slug 전역 UNIQUE · langCHECK 없는 text · status CHECK 7종
digest digest jsonb 1개 — perspective + source 메타. 채워지면 digest 기사
번역 (en) translated_from_article_id · translation_source_hash · translation_issues translated_from_article_id는 self-FK(부모 ko) — en 266행 전건 채움, ko 0건
패스트트랙 fast_track_correction_id · fast_track_requested_at · fast_track_published_at · fast_track_en_correction_id fast_track_published_at은 「사람 검수 없는 발행」 감사 축(양 언어 공용) · …_en_…은 유일하게 언어가 컬럼명에 박힌 자리 → 블로커 ①

AS-IS · 3ko / en 채움 실태 (라이브)

스키마는 동일하고 채워지는 컬럼만 다르다. en을 구별하는 것은 lang='en' + 번역 계보 3컬럼뿐이다.

컬럼ko (469행)en (266행)해석
translated_from_article_id0266en 전건이 ko 부모를 가리킨다
translation_source_hash0266부모 본문 해시 — 부모 변경 감지
translation_issues031en gate_failed 31행과 정확히 일치
digest740digest 소스는 translate_to가 없어 en 형제 미생성
summary395266
fast_track_published_at401224자동 발행 감사 축 — 양 언어 공용

상태 분포: ko는 published 448 · in_review 16 · unpublished 5, en은 published 235 · gate_failed 31 (in_review 0). 9/1 실측 문서 대비 크게 달라진 값으로, ko 자동 발행 활성 사고분 620건 사후 수용(9/2 유지 판정)이 반영된 상태다.

AS-IS · 4원문까지의 연결 구조

원문 본문은 이미 매거진 DB 안에 있다 — raw_items.body_text. 연결은 FK 두 홉으로 완결된다.

sources

소스(=계약) 1건
license_tag · 스위치

source_id

raw_items

원문 사본 — body_text ·
title · publisher · author ·
images · lang(현재 ko만)

cluster_id

clusters

클러스터 1 : raw_items N
(2소스 confirmed 경로)

cluster_id

articles (ko)

재구성 / digest 본문
refs = 원문 URL 배열

translated_from
_article_id

articles (en·ja·zh…)

번역 형제 — self-FK 1:N
언어별 행 추가만으로 확장

⚠️ 정본은 엔진 쪽이다engine.articles(파싱 본문) + GCS raw HTML. raw_items는 어댑터 사본이라 backfill(body_hash 변경) 시 어긋날 수 있다. 라이브 기준 클러스터 469 = ko 기사 469로 1:1이지만, 스키마상 클러스터당 raw_items는 N건이다.

AS-IS · 5fast_track — 스위치는 엔진, 상태는 매거진 행

패스트트랙은 테이블이 아니라 두 DB에 걸친 스위치 + 컬럼 4개다. 스위치는 엔진 DB engine.sources.request_fast_track(엔진 0012)에 살고, 대기표·감사 축은 매거진 articles 행 자체에 있다(매거진 0026) — 별도 큐 테이블은 없다(매거진 DB 12테이블 전수 확인).

제휴 언론사 적용 실태 — 3사 전부 켜져 있다

slug매체enabledrequest_fast_track
itdaily아이티데일리truetrue
aitimes인공지능신문truetrue
epnc테크월드뉴스truetrue
itdaily-emptyprobeAC⑥ 테스트 프로브 (제휴사 아님)falsefalse

제휴 3사 전부 적용 — ADR-0022 rev1(패스트트랙 3사 · 근거는 협약 소스 운영 정책)의 문면과 라이브가 일치한다. 프로브 행은 T6 데모용 복제 소스로 비활성이다.

매거진 쪽 4컬럼의 역할 (0026 헤더 기준)

컬럼역할
fast_track_correction_id이 컬럼이 곧 대기표(queue)다 — non-null = 처리 중, 스윕이 발행·통지까지 끝내면 NULL 복귀. 엔진 correction_requests.id의 크로스-DB 참조라 FK가 없다. 라이브 pending 0건
fast_track_requested_at접수 시각(api가 찍음) — 계약 제8조① 「즉시」의 증빙 시작점. 발행 시각과의 차가 지연 실측
fast_track_published_at「사람 검수 없는 발행」의 유일한 감사 축 — 불변 원칙 4의 예외가 몇 건인지 여기서만 센다. published_at과 다른 사실(저쪽은 최초 발행 시각)
fast_track_en_correction_iden 형제로 정정을 잇는 일회용 배턴(ADR-0022 D6) — 블로커 ①의 그 컬럼

발행 경로 판별 실측 — 감사 축 분해

같은 fast_track_published_at이라도 fast_track_requested_at 유무로 경로가 갈린다: 있음 = 정정 패스트트랙(ADR-0022), 없음 = 자동 발행(ko는 ADR-0025 · en은 스윕 ②).

langft 발행 계정정 경로자동 발행 경로처리 중(pending)
ko40123990
en22402240

정정 패스트트랙이 실제로 쓰인 것은 ko 2건뿐이고, 나머지 623건은 자동 발행 경로다. fast_track_en_correction_id는 라이브 채움 0건(일회용 배턴 — 스윕이 소비 후 NULL 복귀)이지만, 경로 자체는 살아 있는 코드라 블로커 ①의 재설계 대상이다.

TO-BE요구 3건 × 스키마 판정

요구판정스키마 작업스키마 밖 작업
원문을 응답에 포함 JOIN으로 가능 없음 — articles.cluster_id → raw_items 정책 결정 3건 (아래) + 응답 조립
digest 구분 제공 이미 충족 없음 — digest jsonb + API 파생 필드 기존재 없음
ja·zh 언어 추가 구조는 됨 · 걸림 2 블로커 ①② (아래) — 그 외 DDL 0 화이트리스트·프롬프트·SERVED_LANGS·models.yaml

원문 응답의 정책 결정 3건 (스키마 아님 · 회의 안건)

  1. 원문은 배열로. 클러스터 1 : raw_items N이 정상 경로(2소스 confirmed)이므로 응답 필드는 단수가 아니라 배열이어야 한다.
  2. 정본 선택. 매거진 사본(raw_items.body_text) vs 엔진 정본(engine.articles) — backfill로 어긋날 수 있어 서빙 기준을 정해야 한다.
  3. suppression + 계약. takedown 시 원문 필드도 동일 미반환 경로를 타야 하고(AC ④⑤⑫), 원문 전문 노출은 번역 게재와 같은 「원저작물의 새로운 이용」이라 계약 문면 판정 선행 — translate_to처럼 소스별 opt-in 스위치가 맞다. 현 라이브 3사는 전부 license_tag=contract라 후보는 된다.

불변 원칙 1(writer 원문 차단)은 모듈 경계 문제라 API가 원문을 응답에 싣는 것과 무관하다 — 위반 아님.

블로커스키마에서 실제로 걸리는 곳 둘

fast_track_en_correction_id — 언어가 컬럼명에 박힌 유일한 자리

ko 부모 행에서 en 형제의 정정 배턴을 거는 용도다. ja·zh를 더하면 fast_track_ja_correction_id·fast_track_zh_correction_id… 로 언어마다 컬럼이 증식하는 구조라, 언어 확장 전에 재설계가 필요하다.

대안 축: 배턴을 부모 행의 언어별 컬럼이 아니라 형제 행 자신의 fast_track_correction_id로 옮기거나, jsonb / 별도 테이블로 정규화. 29컬럼 중 이 하나만 언어 중립이 아니다.

사용 위치
packages/transitions/magazine_transitions/fast_track.py:160
영향 범위
ADR-0022 정정 패스트트랙의 en 배턴 경로 (D6)

(translated_from_article_id, lang) 유니크 인덱스 부재

현재 articles_translated_from_idx비유니크 부분 인덱스다. 언어가 en 하나일 때는 스윕 코드가 중복 형제를 막지만, 언어별 스윕이 병행되면 같은 부모에 형제가 이중 생성되는 것을 DB가 막아주지 않는다.

이번 확장에서 유일하게 권장하는 DDL 1건:

create unique index articles_sibling_lang_uidx
    on articles (translated_from_article_id, lang)
 where translated_from_article_id is not null;

라이브 데이터는 부모×언어 중복 0건이라 즉시 적용 가능. DDL은 root + dbname=magazine (magazine_app은 owner 아님 — deploy.md 소유자 함정).

참고스키마 밖 — 코드·설정에서 손대는 곳

언어 확장의 나머지는 전부 코드/설정이다. 번역 스윕 자체는 이미 lang_to 파라미터로 짜여 있어 언어별 호출로 확장된다.

위치현재ja·zh 추가 시
packages/connectors/connectors/config.pyTRANSLATE_TO_ALLOWED = {"en"} 화이트리스트 · translate_to는 이미 list[str]화이트리스트 확장 (계약 판단 선행 — ADR-0020 D8)
pipeline/writer/translate.py_PROMPTS("ko","en") 쌍만 · 부록 로컬라이즈에 lang_to=="en" 분기 (551·632행)언어쌍 지시문 + 로컬라이즈 분기 추가
apps/api/engine_api/magazine.pySERVED_LANGS · DEFAULT_LANG="ko"서빙 언어 추가
config/models.yamltranslate 단계 모델 (정본 — ADR-0003 rev3)언어쌍별 모델이 갈리면 여기서 (ADR-0020 소관)

ja 추가는 JP 시장 트랙 동결과 별개 축이다 — KR 시장 기사의 일본어 번역(lang=ja · market=KR)이지 JP 시장 자체 생성이 아니고, translate 경로는 langpack/ja를 거치지 않는다(무접촉 유지). 단 새 언어쌍은 계약 판단이 선행한다.