← 목록으로
2026-02-25plans

title: content-orchestration DB 설계서 (L1) date: 2026-02-25T15:45:00+09:00 type: design layer: L1 status: in-review task_id: "dc1d0d04-5bca-4d69-abc7-e0b4f3e40c79" tags: [content-orchestration, db-design, L1, turso, libsql] author: design-db-pl project: content-orchestration reviewed_by: "jarvis" reviewed_at: "2026-02-25T06:55:00+09:00" approved_by: "" approved_at: ""

content-orchestration DB 설계서 (L1)

Task ID: dc1d0d04-5bca-4d69-abc7-e0b4f3e40c79 작성일: 2026-02-25 작성자: design-db-pl 근거 문서: 2026-02-25-content-orchestration-biz-v4.md (L0 비즈 기획서)


1. 현황 분석

1-1. 현재 content-os Turso DB 테이블 현황

content-orchestration은 content-os Turso DB(libsql://content-os-...turso.io)에 연결되어 있으며, 아래 6개 테이블이 존재한다. 스키마 원본은 content-pipeline/schema/content-os.sql에 정의되어 있다.

#테이블건수용도활용 상태
1collected_news178건RSS 수집 기사 원본활성 (RSS 수집 파이프라인 정상)
2newsletters2건뉴스레터 콘텐츠부분 활성 (생성 가능, 발송 미연동)
3content_queue0건콘텐츠 생성 대기열미사용 (테이블만 존재, 등록/관리 기능 없음)
4pipeline_logs0건파이프라인 실행 기록미사용 (로그 기록 미구현)
5content_logs0건콘텐츠 발행 기록미사용 (발행 추적 미구현)
6plf_schedule7건PLF 런칭 스케줄활성 (캘린더에 표시)

1-2. 기존 테이블 컬럼 상세

collected_news (178건)

컬럼타입NULL설명
idTEXT PKNOUUID v4 (auto)
titleTEXTNO기사 제목
urlTEXT UNIQUENO기사 원본 URL
sourceTEXTNORSS 소스명 (예: "TechCrunch AI")
langTEXTYES'en' / 'ko' (기본값: 'en')
gradeTEXTYES'S' / 'A' / 'B' (기본값: 'A')
categoryTEXTYES'news' / 'official' / 'community' / 'research'
summaryTEXTYESAI 요약
content_snippetTEXTYES기사 본문 일부
published_atTEXTYESISO 8601 문자열 (RSS 원본 날짜)
used_in_newsletterINTEGERYES0=미사용, 1=사용됨 (기본값: 0)
created_atINTEGERYESms epoch (기본값: unixepoch()*1000)

인덱스: idx_collected_news_used, idx_collected_news_source, idx_collected_news_created

newsletters (2건)

컬럼타입NULL설명
idTEXT PKNOUUID v4
subjectTEXTNO뉴스레터 제목
html_contentTEXTNOHTML 본문
plain_contentTEXTYES텍스트 본문
statusTEXTYES'draft' / 'ready' / 'sent' / 'failed' (기본값: 'draft')
email_service_idTEXTYES이메일 서비스 ID (Brevo 등)
sent_atINTEGERYES발송 시각 (ms epoch)
created_atINTEGERYESms epoch

인덱스: idx_newsletters_status, idx_newsletters_created

content_queue (0건)

컬럼타입NULL설명
idTEXT PKNOUUID v4
typeTEXTNO'newsletter' / 'blog' / 'sns'
pillarTEXTYES콘텐츠 필라 (AI도구리뷰, 업종별AI가이드 등)
topicTEXTYES콘텐츠 주제
statusTEXTYES'pending' / 'processing' / 'completed' / 'failed' (기본값: 'pending')
priorityINTEGERYES높을수록 우선 (기본값: 0)
result_idTEXTYES생성 결과물 ID (newsletters.id 등)
error_messageTEXTYES실패 시 에러
created_atINTEGERYESms epoch
updated_atINTEGERYESms epoch
scheduled_atINTEGERYES예약 발행 시간 (ALTER TABLE로 추가)
channelTEXTYES배포 채널 (ALTER TABLE로 추가)
projectTEXTYES프로젝트명 (ALTER TABLE로 추가)

인덱스: idx_content_queue_status, idx_content_queue_type, idx_content_queue_priority

pipeline_logs (0건)

컬럼타입NULL설명
idTEXT PKNOUUID v4
pipeline_nameTEXTNO'collect' / 'generate' / 'publish' / 'blog' / 'sns'
statusTEXTNO'started' / 'completed' / 'failed'
duration_msINTEGERYES실행 소요 시간 (ms)
items_processedINTEGERYES처리 건수 (기본값: 0)
error_messageTEXTYES실패 에러
metadataTEXTYESJSON 문자열 (예: {"feeds_ok": 15, "feeds_fail": 2})
created_atINTEGERYESms epoch

인덱스: idx_pipeline_logs_name, idx_pipeline_logs_status, idx_pipeline_logs_created

content_logs (0건)

컬럼타입NULL설명
idTEXT PKNOUUID v4
content_typeTEXTNO'blog' / 'newsletter' / 'sns'
content_idTEXTYESFK (논리적, newsletters.id 등)
titleTEXTYES콘텐츠 제목
platformTEXTYES배포 플랫폼 ('blog.apppro.kr', 'brevo' 등)
statusTEXTYES'published' / 'draft' / 'failed' (기본값: 'published')
metricsTEXTYESJSON 문자열 ({"views": 0, "clicks": 0})
published_atINTEGERYESms epoch
created_atINTEGERYESms epoch

인덱스: idx_content_logs_type, idx_content_logs_platform, idx_content_logs_published

plf_schedule (7건)

컬럼타입NULL설명
idTEXT PKNOUUID v4
content_typeTEXTNO콘텐츠 유형
titleTEXTNO제목
slug_or_fileTEXTYES슬러그/파일 참조
channelTEXTYES배포 채널
scheduled_weekTEXTYES예정 주차
scheduled_dayTEXTYES예정 요일
statusTEXTYES기본값: 'draft'
created_atINTEGERYESms epoch

1-3. 현재 한계점

  1. 콘텐츠 생성→배포 연결 부재: content_queue에서 생성된 콘텐츠가 어느 채널에 배포되었는지 추적 불가
  2. 채널 정보 하드코딩: 코드 내 채널 목록이 하드코딩되어 있음 (블로그, Brevo, getlate 등). 동적 관리 불가
  3. 승인 플로우 미구현: content_queue.status에 승인 관련 상태('reviewing', 'approved')가 없음
  4. 2-Layer 상태 추적 불가: 내부 DB 상태와 외부 플랫폼 등록 상태를 분리 추적할 수 없음
  5. 에러 추적 산재: 에러가 각 테이블의 error_message에 흩어져 있어 통합 모니터링 불가
  6. 성과 기반 최적화 이력 부재: AI 생성 파라미터 변경 이력과 그 효과를 추적할 수 없음
  7. 빈 테이블 4개: content_queue, pipeline_logs, content_logs에 데이터가 없어 대시보드에서 빈 화면 표시

참고: content_logs(기존)와 content_distributions(신규) 역할 구분

  • content_logs: 발행 이벤트의 불변 기록(append-only 로그). 한번 INSERT되면 수정하지 않는 감사(audit) 성격의 테이블.
  • content_distributions: 채널별 배포 상태의 실시간 추적(상태 UPDATE 가능). pending → registered → published로 상태가 변경되며, 대시보드에서 현재 상태를 보여준다.
  • 목적이 다르므로 중복이 아니며, content_logs는 "무엇이 발행되었는가"의 이력, content_distributions는 "지금 어떤 상태인가"의 현황을 담당한다.

2. 신규 테이블 설계

2-1. channels (채널/계정/매체 관리)

목적: 블로그, 뉴스레터, SNS, 외부 플랫폼 등 모든 콘텐츠 배포 채널을 동적으로 관리한다. 현재 하드코딩된 채널 목록을 DB 기반으로 전환하여, 채널 추가/삭제/설정 변경을 코드 수정 없이 가능하게 한다.

사용 시나리오:

  • 새 SNS 채널 개설 시 DB에 등록 → 대시보드에 자동 노출
  • 채널별 API 인증 정보 저장 → 자동 배포 시 참조
  • 채널별 발행 규칙(빈도, 시간대, 형식) 설정 → 스케줄러가 참조
컬럼타입NULL기본값설명예시값
idTEXT PKNOUUID auto채널 고유 ID"ch-001"
nameTEXTNO채널 표시명"AppPro 블로그"
typeTEXTNO채널 유형: 'blog' / 'newsletter' / 'sns' / 'external'"blog"
platformTEXTNO플랫폼 식별자"apppro.kr"
projectTEXTYESNULL소속 프로젝트 (NULL이면 공통)"apppro"
configTEXTYESNULLJSON 문자열: API URL, 발행 빈도, 시간대, 콘텐츠 형식 등'{"publish_url":"/api/cron/publish","timezone":"Asia/Seoul","format":"markdown"}'
credentials_refTEXTYESNULL환경 변수 키 참조 (실제 값 저장 금지)"BREVO_API_KEY"
is_activeINTEGERNO1채널 활성 여부 (0=비활성, 1=활성)1
created_atINTEGERNOunixepoch()*1000생성 시각 (ms epoch)1740000000000
updated_atINTEGERNOunixepoch()*1000수정 시각 (ms epoch)1740000000000

인덱스:

  • idx_channels_typetype: 채널 유형별 목록 조회
  • idx_channels_projectproject: 프로젝트별 채널 필터링
  • idx_channels_activeis_active: 활성 채널만 조회

관계: content_distributions.channel_idchannels.id (논리적 FK)

Turso/LibSQL 고려사항:

  • config는 JSON 타입 미지원이므로 TEXT로 저장, 앱 레이어에서 파싱
  • credentials_ref는 환경 변수 키 이름만 저장, 실제 비밀값은 Vercel env에 보관 (보안)

SQL:

CREATE TABLE IF NOT EXISTS channels (
  id              TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6)))),
  name            TEXT NOT NULL,
  type            TEXT NOT NULL,
  platform        TEXT NOT NULL,
  project         TEXT,
  config          TEXT,
  credentials_ref TEXT,
  is_active       INTEGER NOT NULL DEFAULT 1,
  created_at      INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
  updated_at      INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
);

CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project);
CREATE INDEX IF NOT EXISTS idx_channels_active ON channels(is_active);

초기 시드 데이터 (Phase 1):

nametypeplatformproject
AppPro 블로그blogapppro.krapppro
Brevo 뉴스레터newsletterbrevoapppro
Twitter/XsnstwitterNULL (공통)

2-2. content_distributions (채널별 배포 추적, 2-Layer 상태)

목적: 기획서 5-3의 핵심 요구사항인 "2-Layer 상태 관리"를 구현한다. 하나의 콘텐츠가 여러 채널에 배포될 때, 각 채널별 등록/발행 상태를 독립적으로 추적한다. 내부 DB 상태(content_queue.status)와 외부 플랫폼 상태(platform_status)를 분리한다.

사용 시나리오:

  • 콘텐츠 승인(approved) 후 블로그/Brevo/Twitter 3개 채널에 동시 예약 등록
  • 채널별로 등록 성공/실패/발행 완료를 독립 추적
  • 대시보드에서 "3/3 채널 등록 완료" 같은 통합 현황 표시
컬럼타입NULL기본값설명예시값
idTEXT PKNOUUID auto배포 고유 ID"dist-001"
content_idTEXTNOcontent_queue.id 참조"cq-001"
channel_idTEXTNOchannels.id 참조"ch-001"
platform_statusTEXTNO'pending''pending' / 'registered' / 'published' / 'failed'"registered"
platform_idTEXTYESNULL외부 플랫폼 ID (campaign_id, post_id 등)"campaign_abc123"
platform_urlTEXTYESNULL발행된 URL"https://blog.apppro.kr/posts/ai-trend-2026"
scheduled_atINTEGERYESNULL예약 발행 시간 (ms epoch)1740100000000
published_atINTEGERYESNULL실제 발행 시간 (ms epoch)1740100060000
error_messageTEXTYESNULL실패 시 에러 메시지"Brevo API 401 Unauthorized"
retry_countINTEGERNO0자동 재시도 횟수2
created_atINTEGERNOunixepoch()*1000등록 시각1740000000000
updated_atINTEGERNOunixepoch()*1000수정 시각1740000000000

인덱스:

  • idx_distributions_contentcontent_id: 콘텐츠별 배포 현황 조회 (대시보드 핵심 쿼리)
  • idx_distributions_channelchannel_id: 채널별 배포 통계
  • idx_distributions_statusplatform_status: 실패/대기 건 필터링
  • idx_distributions_scheduledscheduled_at: 발행 예정 스케줄 조회

관계:

  • content_idcontent_queue.id (논리적 FK)
  • channel_idchannels.id (논리적 FK)

중복 레코드 정책: 동일 content_id + channel_id 조합에 대해 UNIQUE 제약을 두지 않는다. 이는 의도적인 설계로, 배포 실패 후 재시도 시 새 레코드를 INSERT하여 이력을 추적한다. 최신 레코드(MAX(created_at))가 현재 상태이며, 이전 레코드는 재시도 이력으로 보존된다.

SQL:

CREATE TABLE IF NOT EXISTS content_distributions (
  id              TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6)))),
  content_id      TEXT NOT NULL,
  channel_id      TEXT NOT NULL,
  platform_status TEXT NOT NULL DEFAULT 'pending',
  platform_id     TEXT,
  platform_url    TEXT,
  scheduled_at    INTEGER,
  published_at    INTEGER,
  error_message   TEXT,
  retry_count     INTEGER NOT NULL DEFAULT 0,
  created_at      INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
  updated_at      INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
);

CREATE INDEX IF NOT EXISTS idx_distributions_content ON content_distributions(content_id);
CREATE INDEX IF NOT EXISTS idx_distributions_channel ON content_distributions(channel_id);
CREATE INDEX IF NOT EXISTS idx_distributions_status ON content_distributions(platform_status);
CREATE INDEX IF NOT EXISTS idx_distributions_scheduled ON content_distributions(scheduled_at);

2-3. error_logs (자체교정 에러 로그)

목적: 기획서 7-3의 자체교정(Self-Healing) 시스템을 위한 통합 에러 로그. 파이프라인 실패, AI 생성 품질 미달, 배포 실패 등 모든 에러를 한 곳에 수집하고, 자동 교정 시도 결과를 기록한다.

사용 시나리오:

  • RSS 수집 실패 → error_logs에 기록 → 자동 교정 시도(URL 교체) → 결과 기록
  • AI 생성 품질 미달 → QA 미통과 로그 → 프롬프트 파라미터 조정 → 재생성 결과 기록
  • 대시보드에서 "최근 7일 자동 교정: 10/12건 성공 (83%)" 표시
컬럼타입NULL기본값설명예시값
idTEXT PKNOUUID auto에러 로그 ID"err-001"
occurred_atINTEGERNOunixepoch()*1000에러 발생 시각1740000000000
componentTEXTNO발생 컴포넌트"rss_collector"
error_typeTEXTNO에러 유형"timeout"
error_messageTEXTNO원본 에러 메시지"HTTP 503 Service Unavailable"
content_idTEXTYESNULL관련 콘텐츠 ID"cq-001"
channel_idTEXTYESNULL관련 채널 ID"ch-002"
auto_fix_attemptedINTEGERNO0자동 교정 시도 여부 (0/1)1
auto_fix_resultTEXTYESNULL'success' / 'failed' / 'skipped'"success"
auto_fix_actionTEXTYESNULL교정 액션 설명"RSS URL 자동 교체: old.xml → new.xml"
escalatedINTEGERNO0에스컬레이션 여부 (0/1)0
resolved_atINTEGERYESNULL해결 시각 (ms epoch)1740000060000
resolution_typeTEXTYESNULL'auto_fixed' / 'manual_fixed' / 'ignored'"auto_fixed"

인덱스:

  • idx_error_logs_occurredoccurred_at: 시간순 조회
  • idx_error_logs_componentcomponent: 컴포넌트별 에러 집계
  • idx_error_logs_typeerror_type: 에러 유형별 필터
  • idx_error_logs_unresolvedresolved_at: NULL인 미해결 에러 조회

component 값 목록: rss_collector, ai_generator, publisher, qa_checker, scheduler, brevo, sns_publisher error_type 값 목록: timeout, auth_fail, quality_fail, api_error, build_fail, rate_limit, validation_fail

SQL:

CREATE TABLE IF NOT EXISTS error_logs (
  id                  TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6)))),
  occurred_at         INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
  component           TEXT NOT NULL,
  error_type          TEXT NOT NULL,
  error_message       TEXT NOT NULL,
  content_id          TEXT,
  channel_id          TEXT,
  auto_fix_attempted  INTEGER NOT NULL DEFAULT 0,
  auto_fix_result     TEXT,
  auto_fix_action     TEXT,
  escalated           INTEGER NOT NULL DEFAULT 0,
  resolved_at         INTEGER,
  resolution_type     TEXT
);

CREATE INDEX IF NOT EXISTS idx_error_logs_occurred ON error_logs(occurred_at);
CREATE INDEX IF NOT EXISTS idx_error_logs_component ON error_logs(component);
CREATE INDEX IF NOT EXISTS idx_error_logs_type ON error_logs(error_type);
CREATE INDEX IF NOT EXISTS idx_error_logs_unresolved ON error_logs(resolved_at);

2-4. optimization_history (성과 기반 최적화 이력)

목적: 기획서 7-2 "성과 기반 자동 최적화"의 이력을 기록한다. AI 생성 파라미터, 발행 시간, 채널 배분 등을 변경할 때마다 변경 전후 값과 근거를 기록하고, 변경 효과를 추적한다.

사용 시나리오:

  • 발행 시간을 09:00에서 08:30으로 변경 → 근거: "오전 8시대 오픈율 +5%" → 효과 측정: "개선"
  • 프롬프트 온도를 0.7에서 0.5로 변경 → 근거: "팩트 오류 감소 목적" → 효과: "품질 통과율 70%→85%"
컬럼타입NULL기본값설명예시값
idTEXT PKNOUUID auto이력 ID"opt-001"
changed_atINTEGERNOunixepoch()*1000변경 시각1740000000000
parameterTEXTNO변경 파라미터명"publish_time"
old_valueTEXTYESNULL변경 전 값"09:00"
new_valueTEXTNO변경 후 값"08:30"
reasonTEXTNO변경 근거 (데이터 기반)"오전 8시대 오픈율 +5% (최근 2주 통계)"
impact_measuredINTEGERNO0효과 측정 완료 여부 (0/1)1
impact_resultTEXTYESNULL'improved' / 'degraded' / 'neutral'"improved"
impact_detailTEXTYESNULL효과 상세 설명"오픈율 32%→37%, +5%p 개선"

인덱스:

  • idx_optimization_changedchanged_at: 시간순 조회
  • idx_optimization_paramparameter: 파라미터별 변경 이력 조회

parameter 값 목록: publish_time, prompt_template, prompt_temperature, prompt_max_tokens, target_channel, content_length, title_pattern, topic_pillar

SQL:

CREATE TABLE IF NOT EXISTS optimization_history (
  id                TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6)))),
  changed_at        INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
  parameter         TEXT NOT NULL,
  old_value         TEXT,
  new_value         TEXT NOT NULL,
  reason            TEXT NOT NULL,
  impact_measured   INTEGER NOT NULL DEFAULT 0,
  impact_result     TEXT,
  impact_detail     TEXT
);

CREATE INDEX IF NOT EXISTS idx_optimization_changed ON optimization_history(changed_at);
CREATE INDEX IF NOT EXISTS idx_optimization_param ON optimization_history(parameter);

2-5. content_generation_config (AI 생성 파라미터 관리)

목적: 기획서 7-2의 AI 콘텐츠 생성 파라미터를 DB에서 관리한다. 프롬프트 템플릿, 모델 설정, 품질 기준 등을 코드 수정 없이 동적으로 변경 가능하게 한다.

사용 시나리오:

  • 콘텐츠 유형별(blog, newsletter, sns) 다른 생성 파라미터 설정
  • 프로젝트별 스타일/톤 차별화 (apppro는 전문적, richbukae는 친근)
  • 자동 최적화 시스템이 파라미터를 변경할 때 이 테이블을 UPDATE
컬럼타입NULL기본값설명예시값
idTEXT PKNOUUID auto설정 ID"cfg-001"
content_typeTEXTNO'blog' / 'newsletter' / 'sns'"blog"
projectTEXTYESNULL프로젝트명 (NULL이면 전역 기본값)"apppro"
modelTEXTNO'gemini-flash'AI 모델 식별자"gemini-2.0-flash"
temperatureREALNO0.7생성 온도0.7
max_tokensINTEGERNO2000최대 토큰 수2000
prompt_templateTEXTYESNULL프롬프트 템플릿 (길 경우 TEXT)"당신은 AI 전문 기자입니다..."
style_guideTEXTYESNULL스타일 가이드 (톤, 어투, 형식)'{"tone":"professional","length":"1500-2000자","language":"ko"}'
qa_thresholdREALNO0.7QA 통과 임계값 (0.0~1.0)0.7
is_activeINTEGERNO1활성 여부 (0/1)1
created_atINTEGERNOunixepoch()*1000생성 시각1740000000000
updated_atINTEGERNOunixepoch()*1000수정 시각1740000000000

인덱스:

  • idx_gen_config_type_projectcontent_type, project: 유형+프로젝트별 설정 조회 (핵심 쿼리)
  • idx_gen_config_activeis_active: 활성 설정만 조회

유니크 제약: content_type + project 조합은 유니크해야 함 (하나의 유형/프로젝트에 활성 설정 1개)

SQL:

CREATE TABLE IF NOT EXISTS content_generation_config (
  id                TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6)))),
  content_type      TEXT NOT NULL,
  project           TEXT,
  model             TEXT NOT NULL DEFAULT 'gemini-flash',
  temperature       REAL NOT NULL DEFAULT 0.7,
  max_tokens        INTEGER NOT NULL DEFAULT 2000,
  prompt_template   TEXT,
  style_guide       TEXT,
  qa_threshold      REAL NOT NULL DEFAULT 0.7,
  is_active         INTEGER NOT NULL DEFAULT 1,
  created_at        INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
  updated_at        INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_gen_config_type_project ON content_generation_config(content_type, project) WHERE is_active = 1;
CREATE INDEX IF NOT EXISTS idx_gen_config_active ON content_generation_config(is_active);

3. 기존 테이블 확장

3-1. content_queue 확장 (승인 플로우 추가)

변경 목적: 기획서 2-3 "승인 플로우" 요구사항을 구현하기 위해 content_queue의 status 체계를 확장하고, 승인 관련 필드를 추가한다.

현재 status 값: pending, processing, completed, failed

확장 status 값: draftreviewingapprovedscheduledpublishedfailed

변경컬럼타입설명
ADD COLUMNtitleTEXT콘텐츠 제목 (대시보드 표시용)
ADD COLUMNcontent_bodyTEXT콘텐츠 본문 (승인 전 미리보기용)
ADD COLUMNapproved_byTEXT승인자 ('ceo' / 'auto')
ADD COLUMNapproved_atINTEGER승인 시각 (ms epoch)
ADD COLUMNrejected_reasonTEXT거부 사유 (피드백 루프)

status 전이 규칙:

draft (초안 생성)
  → reviewing (CEO 검수 요청, 텔레그램 알림 발송)
  → approved (CEO 승인 또는 자동 승인 타이머)
  → scheduled (채널별 예약 등록 완료)
  → published (모든 채널 발행 완료)

  → failed (어느 단계에서든 실패 가능)

  reviewing → draft (거부: rejected_reason에 사유 기록)

마이그레이션 SQL (LibSQL은 ALTER TABLE ADD COLUMN만 지원):

ALTER TABLE content_queue ADD COLUMN title TEXT;
ALTER TABLE content_queue ADD COLUMN content_body TEXT;
ALTER TABLE content_queue ADD COLUMN approved_by TEXT;
ALTER TABLE content_queue ADD COLUMN approved_at INTEGER;
ALTER TABLE content_queue ADD COLUMN rejected_reason TEXT;

기존 데이터 영향: content_queue가 현재 0건이므로 데이터 마이그레이션 불필요. 기존 status 값('pending', 'processing', 'completed', 'failed')은 새 체계와 호환 가능 (pending=draft로 매핑).


3-2. pipeline_logs 확장 (자체교정 연계)

변경 목적: pipeline_logs를 error_logs와 연계하여, 파이프라인 실행 실패 시 자동으로 자체교정 프로세스를 시작할 수 있도록 한다.

변경컬럼타입설명
ADD COLUMNerror_log_idTEXT실패 시 error_logs.id 참조
ADD COLUMNtrigger_typeTEXT'manual' / 'scheduled' / 'retry' (실행 유형)
ADD COLUMNparent_log_idTEXT재시도인 경우, 원본 pipeline_logs.id

마이그레이션 SQL:

ALTER TABLE pipeline_logs ADD COLUMN error_log_id TEXT;
ALTER TABLE pipeline_logs ADD COLUMN trigger_type TEXT DEFAULT 'scheduled';
ALTER TABLE pipeline_logs ADD COLUMN parent_log_id TEXT;

기존 데이터 영향: pipeline_logs가 현재 0건이므로 영향 없음.


3-3. newsletters 확장 (배포 연계)

변경 목적: newsletters를 content_distributions와 연계하여, 뉴스레터가 어떤 content_queue 아이템에서 생성되었는지 추적하고, 배포 상태를 연결한다.

변경컬럼타입설명
ADD COLUMNcontent_queue_idTEXT원본 content_queue.id 참조
ADD COLUMNprojectTEXT소속 프로젝트

마이그레이션 SQL:

ALTER TABLE newsletters ADD COLUMN content_queue_id TEXT;
ALTER TABLE newsletters ADD COLUMN project TEXT;

기존 데이터 영향: newsletters에 2건 존재하나, 새 컬럼은 모두 NULL 허용이므로 영향 없음. 기존 2건은 content_queue_id = NULL로 유지 (레거시).


4. 마이그레이션 전략

4-1. 신규 테이블 생성 순서 (의존성 기반)

테이블 간 논리적 FK 의존성에 따른 생성 순서:

1단계: channels               (독립, 의존성 없음)
2단계: content_generation_config  (독립, 의존성 없음)
3단계: content_queue ALTER      (기존 테이블 확장, 독립)
4단계: newsletters ALTER       (기존 테이블 확장, 독립)
5단계: pipeline_logs ALTER     (기존 테이블 확장, 독립)
6단계: content_distributions   (channels, content_queue 의존)
7단계: error_logs              (channels, content_queue 논리적 참조)
8단계: optimization_history    (독립, 마지막 순서)

1~5단계는 서로 의존성이 없으므로 병렬 실행 가능. 6단계는 channels와 content_queue가 존재해야 의미가 있으므로 이후 실행.

4-2. 마이그레이션 실행 방법

-- ===== Phase 1 마이그레이션 스크립트 =====
-- 실행 전: Turso DB 백업 (turso db shell content-os ".dump" > backup.sql)

-- 1단계: 신규 테이블 (CREATE TABLE IF NOT EXISTS — 멱등성 보장)
-- channels
CREATE TABLE IF NOT EXISTS channels (...);
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project);
CREATE INDEX IF NOT EXISTS idx_channels_active ON channels(is_active);

-- content_generation_config
CREATE TABLE IF NOT EXISTS content_generation_config (...);
CREATE UNIQUE INDEX IF NOT EXISTS idx_gen_config_type_project ON content_generation_config(content_type, project) WHERE is_active = 1;
CREATE INDEX IF NOT EXISTS idx_gen_config_active ON content_generation_config(is_active);

-- content_distributions
CREATE TABLE IF NOT EXISTS content_distributions (...);
CREATE INDEX IF NOT EXISTS idx_distributions_content ON content_distributions(content_id);
-- ... (전체 인덱스)

-- error_logs
CREATE TABLE IF NOT EXISTS error_logs (...);
-- ... (전체 인덱스)

-- optimization_history
CREATE TABLE IF NOT EXISTS optimization_history (...);
-- ... (전체 인덱스)

-- 2단계: 기존 테이블 ALTER (각 .catch(() => {}) 패턴으로 이미 존재 시 무시)
ALTER TABLE content_queue ADD COLUMN title TEXT;
ALTER TABLE content_queue ADD COLUMN content_body TEXT;
ALTER TABLE content_queue ADD COLUMN approved_by TEXT;
ALTER TABLE content_queue ADD COLUMN approved_at INTEGER;
ALTER TABLE content_queue ADD COLUMN rejected_reason TEXT;

ALTER TABLE pipeline_logs ADD COLUMN error_log_id TEXT;
ALTER TABLE pipeline_logs ADD COLUMN trigger_type TEXT DEFAULT 'scheduled';
ALTER TABLE pipeline_logs ADD COLUMN parent_log_id TEXT;

ALTER TABLE newsletters ADD COLUMN content_queue_id TEXT;
ALTER TABLE newsletters ADD COLUMN project TEXT;

-- 3단계: 시드 데이터 (Phase 1 필수 채널)
INSERT OR IGNORE INTO channels (id, name, type, platform, project, config, is_active)
VALUES
  ('ch-apppro-blog', 'AppPro 블로그', 'blog', 'apppro.kr', 'apppro', '{"publish_api":"/api/cron/publish","auto_publish":true}', 1),
  ('ch-brevo', 'Brevo 뉴스레터', 'newsletter', 'brevo', 'apppro', '{"list_id":8,"template":"weekly"}', 1),
  ('ch-twitter', 'Twitter/X', 'sns', 'twitter', NULL, '{"max_chars":280}', 0);

4-3. 롤백 전략

신규 테이블: DROP TABLE IF EXISTS {테이블명} — 데이터 손실 없음 (신규이므로) ALTER TABLE 컬럼 추가: LibSQL은 DROP COLUMN을 지원하지 않음. 롤백 시:

  • 추가된 컬럼은 그대로 두되 앱 코드에서 참조하지 않으면 무해
  • 완전 롤백이 필요하면: 새 테이블 생성 → 데이터 복사 → 구 테이블 DROP → 테이블 이름 변경 (복잡, 비권장)
  • 권장: ALTER 전 .dump 백업, 문제 시 백업에서 복원

4-4. 안전 장치

  1. 모든 CREATE는 IF NOT EXISTS: 중복 실행 안전
  2. ALTER TABLE은 .catch(() => {}): 컬럼 이미 존재 시 무시 (기존 content-db.ts 패턴 유지)
  3. 실행 전 백업 필수: turso db shell content-os ".dump" > backup-{날짜}.sql
  4. 트랜잭션 권장: 가능하면 BEGIN/COMMIT으로 원자적 실행

5. ERD (텍스트 형식)

┌─────────────────────┐
│   collected_news     │  178건, RSS 수집 원본
│   (기존, 변경 없음)    │
└──────────┬──────────┘
           │ (소스 데이터)
           ▼
┌─────────────────────┐     ┌─────────────────────────────┐
│   content_queue      │────→│   content_generation_config  │
│   (기존 + 확장)       │     │   (신규)                     │
│                     │     │   콘텐츠 유형별 AI 생성 설정    │
│  + title            │     └─────────────────────────────┘
│  + content_body     │
│  + approved_by/at   │
│  + rejected_reason  │
└──────┬──────┬───────┘
       │      │
       │      │ content_id
       │      ▼
       │  ┌──────────────────────┐     ┌──────────────┐
       │  │ content_distributions │────→│   channels    │
       │  │ (신규)                │     │   (신규)      │
       │  │                      │     │              │
       │  │ 2-Layer 상태 추적     │     │ 채널 동적 관리 │
       │  │ platform_status      │     └──────────────┘
       │  │ platform_id/url      │
       │  └──────┬───────────────┘
       │         │
       │         │ (에러 발생 시)
       │         ▼
       │  ┌──────────────────────┐
       │  │     error_logs        │
       │  │     (신규)            │
       │  │                      │
       │  │ 통합 에러 로그         │
       │  │ 자동 교정 시도/결과    │
       │  └──────────────────────┘
       │
       │ result_id
       ▼
┌─────────────────────┐     ┌──────────────────────────┐
│    newsletters       │     │   optimization_history    │
│    (기존 + 확장)      │     │   (신규)                  │
│                     │     │                          │
│  + content_queue_id  │     │ 파라미터 변경 이력          │
│  + project          │     │ 효과 측정                  │
└─────────────────────┘     └──────────────────────────┘

┌─────────────────────┐     ┌──────────────────────┐
│   pipeline_logs      │     │    plf_schedule       │
│   (기존 + 확장)       │     │    (기존, 변경 없음)   │
│                     │     │    7건                 │
│  + error_log_id     │     └──────────────────────┘
│  + trigger_type     │
│  + parent_log_id    │     ┌──────────────────────┐
└─────────────────────┘     │    content_logs       │
                            │    (기존, 변경 없음)   │
                            │    0건                 │
                            └──────────────────────┘

핵심 데이터 흐름:

collected_news (RSS 원본)
     ↓ AI 생성
content_queue (승인 플로우: draft → reviewing → approved)
     ↓ 승인 후
content_distributions (채널별 예약 등록 → 발행 추적)
     ↓ 발행 시
channels (어디에 발행할지)
     ↓ 문제 발생 시
error_logs (에러 기록 → 자동 교정)
     ↓ 교정 파라미터 변경 시
optimization_history (변경 이력 → 효과 측정)

6. Phase별 구현 우선순위

Phase 1 (MVP, 1주) — 필수 테이블

순서테이블작업이유
1channels신규 생성 + 시드채널 관리 기초, 배포 대상 정의
2content_queue 확장ALTER 5개 컬럼승인 플로우의 핵심
3content_distributions신규 생성2-Layer 상태 추적 (MVP 완료 기준)
4newsletters 확장ALTER 2개 컬럼content_queue 연계
5error_logs신규 생성에러 기록 기초 (L1 자동 재시도용)

Phase 2 (2주) — 확장 테이블

순서테이블작업이유
6content_generation_config신규 생성AI 파라미터 동적 관리
7pipeline_logs 확장ALTER 3개 컬럼자체교정 연계
8optimization_history신규 생성성과 기반 최적화 이력

Phase 3 (3주+) — 데이터 고도화

  • channels: 외부 플랫폼 채널 추가 (티스토리, 미디엄, EO 등)
  • content_generation_config: 프로젝트별/채널별 세분화 설정
  • optimization_history: 자동 A/B 테스트 이력 추가
  • content_distributions: 성과 metrics 컬럼 추가 (views, clicks, conversions)

7. 전체 테이블 요약

최종 테이블 구성 (Phase 1 완료 후)

#테이블상태역할
1collected_news기존 유지RSS 수집 원본 저장
2newsletters기존 + 확장뉴스레터 콘텐츠 + 큐 연계
3content_queue기존 + 확장승인 플로우 중심 콘텐츠 관리
4content_logs기존 유지콘텐츠 발행 이벤트 기록
5pipeline_logs기존 유지 (Phase 2에서 확장)파이프라인 실행 로그
6plf_schedule기존 유지PLF 런칭 스케줄
7channels신규채널/계정/매체 동적 관리
8content_distributions신규채널별 배포 추적, 2-Layer 상태
9error_logs신규통합 에러 로그, 자체교정
10content_generation_config신규 (Phase 2)AI 생성 파라미터 관리
11optimization_history신규 (Phase 2)성과 기반 최적화 이력

Turso/LibSQL 공통 규칙 (기존 스키마와 동일)

  • ID: TEXT PRIMARY KEY, UUID v4 자동 생성 (lower(hex(randomblob(...))))
  • Timestamp: INTEGER, millisecond epoch (unixepoch() * 1000)
  • Boolean: INTEGER (0/1)
  • JSON: TEXT (앱 레이어에서 JSON.parse/JSON.stringify)
  • FK: 논리적 참조만 (LibSQL에서 FK enforcement 불안정, 앱에서 관리)

리뷰 로그

[design-db-pl 초안 작성] 2026-02-25 15:45

  • 기존 6개 테이블 현황 분석 완료 (content-os.sql 스키마 + content-db.ts 코드 기반)
  • 신규 5개 테이블 설계: channels, content_distributions, error_logs, optimization_history, content_generation_config
  • 기존 3개 테이블 확장: content_queue (승인 플로우 5컬럼), pipeline_logs (자체교정 3컬럼), newsletters (연계 2컬럼)
  • 마이그레이션 전략: 의존성 기반 8단계 순서, 롤백 방안 포함
  • Phase별 우선순위: Phase 1 필수 5개 / Phase 2 확장 3개
  • 자비스 1차 검수 요청

[자비스 1차 검수] 2026-02-25 06:55

  • ✅ 기획서 v4 요구사항 커버리지 확인 (채널 관리, 승인 플로우, 2-Layer 상태, 자체교정, AI 파라미터)
  • ✅ Turso/LibSQL 규칙 준수 (JSON→TEXT, Boolean→INTEGER, FK 논리적, UUID auto)
  • ✅ planning-rules.md 준수 (frontmatter, 리뷰 로그, 한글 작성)
  • ✅ Phase별 우선순위 기획서 Phase 1 MVP 기준과 일치
  • ✅ 마이그레이션 안전 장치 확인 (IF NOT EXISTS, .catch 패턴, 백업 지시)
  • ⚠️ 명확화 권고 1: content_logs(기존)와 content_distributions(신규) 역할 구분 — 섹션 1-3에 설명 추가 완료
  • ⚠️ 명확화 권고 2: content_distributions에 동일 content_id+channel_id 중복 레코드 허용 의도 명시 — 섹션 2-2에 설명 추가 완료
  • 결과: 수정 요청 (revision) → 2건 보완 완료

[design-db-pl revision 반영] 2026-02-25 06:58

  • 명확화 1: 섹션 1-3에 content_logs vs content_distributions 역할 구분 설명 추가 (불변 로그 vs 실시간 추적)
  • 명확화 2: 섹션 2-2에 중복 레코드 정책 설명 추가 (UNIQUE 없음 = 재시도 이력 보존 목적)
  • frontmatter reviewed_by/reviewed_at 업데이트 완료
  • VP 보고 대기
plans/2026/02/25/content-orchestration-design-db.md