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에 정의되어 있다.
| # | 테이블 | 건수 | 용도 | 활용 상태 |
|---|---|---|---|---|
| 1 | collected_news | 178건 | RSS 수집 기사 원본 | 활성 (RSS 수집 파이프라인 정상) |
| 2 | newsletters | 2건 | 뉴스레터 콘텐츠 | 부분 활성 (생성 가능, 발송 미연동) |
| 3 | content_queue | 0건 | 콘텐츠 생성 대기열 | 미사용 (테이블만 존재, 등록/관리 기능 없음) |
| 4 | pipeline_logs | 0건 | 파이프라인 실행 기록 | 미사용 (로그 기록 미구현) |
| 5 | content_logs | 0건 | 콘텐츠 발행 기록 | 미사용 (발행 추적 미구현) |
| 6 | plf_schedule | 7건 | PLF 런칭 스케줄 | 활성 (캘린더에 표시) |
1-2. 기존 테이블 컬럼 상세
collected_news (178건)
| 컬럼 | 타입 | NULL | 설명 |
|---|---|---|---|
| id | TEXT PK | NO | UUID v4 (auto) |
| title | TEXT | NO | 기사 제목 |
| url | TEXT UNIQUE | NO | 기사 원본 URL |
| source | TEXT | NO | RSS 소스명 (예: "TechCrunch AI") |
| lang | TEXT | YES | 'en' / 'ko' (기본값: 'en') |
| grade | TEXT | YES | 'S' / 'A' / 'B' (기본값: 'A') |
| category | TEXT | YES | 'news' / 'official' / 'community' / 'research' |
| summary | TEXT | YES | AI 요약 |
| content_snippet | TEXT | YES | 기사 본문 일부 |
| published_at | TEXT | YES | ISO 8601 문자열 (RSS 원본 날짜) |
| used_in_newsletter | INTEGER | YES | 0=미사용, 1=사용됨 (기본값: 0) |
| created_at | INTEGER | YES | ms epoch (기본값: unixepoch()*1000) |
인덱스: idx_collected_news_used, idx_collected_news_source, idx_collected_news_created
newsletters (2건)
| 컬럼 | 타입 | NULL | 설명 |
|---|---|---|---|
| id | TEXT PK | NO | UUID v4 |
| subject | TEXT | NO | 뉴스레터 제목 |
| html_content | TEXT | NO | HTML 본문 |
| plain_content | TEXT | YES | 텍스트 본문 |
| status | TEXT | YES | 'draft' / 'ready' / 'sent' / 'failed' (기본값: 'draft') |
| email_service_id | TEXT | YES | 이메일 서비스 ID (Brevo 등) |
| sent_at | INTEGER | YES | 발송 시각 (ms epoch) |
| created_at | INTEGER | YES | ms epoch |
인덱스: idx_newsletters_status, idx_newsletters_created
content_queue (0건)
| 컬럼 | 타입 | NULL | 설명 |
|---|---|---|---|
| id | TEXT PK | NO | UUID v4 |
| type | TEXT | NO | 'newsletter' / 'blog' / 'sns' |
| pillar | TEXT | YES | 콘텐츠 필라 (AI도구리뷰, 업종별AI가이드 등) |
| topic | TEXT | YES | 콘텐츠 주제 |
| status | TEXT | YES | 'pending' / 'processing' / 'completed' / 'failed' (기본값: 'pending') |
| priority | INTEGER | YES | 높을수록 우선 (기본값: 0) |
| result_id | TEXT | YES | 생성 결과물 ID (newsletters.id 등) |
| error_message | TEXT | YES | 실패 시 에러 |
| created_at | INTEGER | YES | ms epoch |
| updated_at | INTEGER | YES | ms epoch |
| scheduled_at | INTEGER | YES | 예약 발행 시간 (ALTER TABLE로 추가) |
| channel | TEXT | YES | 배포 채널 (ALTER TABLE로 추가) |
| project | TEXT | YES | 프로젝트명 (ALTER TABLE로 추가) |
인덱스: idx_content_queue_status, idx_content_queue_type, idx_content_queue_priority
pipeline_logs (0건)
| 컬럼 | 타입 | NULL | 설명 |
|---|---|---|---|
| id | TEXT PK | NO | UUID v4 |
| pipeline_name | TEXT | NO | 'collect' / 'generate' / 'publish' / 'blog' / 'sns' |
| status | TEXT | NO | 'started' / 'completed' / 'failed' |
| duration_ms | INTEGER | YES | 실행 소요 시간 (ms) |
| items_processed | INTEGER | YES | 처리 건수 (기본값: 0) |
| error_message | TEXT | YES | 실패 에러 |
| metadata | TEXT | YES | JSON 문자열 (예: {"feeds_ok": 15, "feeds_fail": 2}) |
| created_at | INTEGER | YES | ms epoch |
인덱스: idx_pipeline_logs_name, idx_pipeline_logs_status, idx_pipeline_logs_created
content_logs (0건)
| 컬럼 | 타입 | NULL | 설명 |
|---|---|---|---|
| id | TEXT PK | NO | UUID v4 |
| content_type | TEXT | NO | 'blog' / 'newsletter' / 'sns' |
| content_id | TEXT | YES | FK (논리적, newsletters.id 등) |
| title | TEXT | YES | 콘텐츠 제목 |
| platform | TEXT | YES | 배포 플랫폼 ('blog.apppro.kr', 'brevo' 등) |
| status | TEXT | YES | 'published' / 'draft' / 'failed' (기본값: 'published') |
| metrics | TEXT | YES | JSON 문자열 ({"views": 0, "clicks": 0}) |
| published_at | INTEGER | YES | ms epoch |
| created_at | INTEGER | YES | ms epoch |
인덱스: idx_content_logs_type, idx_content_logs_platform, idx_content_logs_published
plf_schedule (7건)
| 컬럼 | 타입 | NULL | 설명 |
|---|---|---|---|
| id | TEXT PK | NO | UUID v4 |
| content_type | TEXT | NO | 콘텐츠 유형 |
| title | TEXT | NO | 제목 |
| slug_or_file | TEXT | YES | 슬러그/파일 참조 |
| channel | TEXT | YES | 배포 채널 |
| scheduled_week | TEXT | YES | 예정 주차 |
| scheduled_day | TEXT | YES | 예정 요일 |
| status | TEXT | YES | 기본값: 'draft' |
| created_at | INTEGER | YES | ms epoch |
1-3. 현재 한계점
- 콘텐츠 생성→배포 연결 부재:
content_queue에서 생성된 콘텐츠가 어느 채널에 배포되었는지 추적 불가 - 채널 정보 하드코딩: 코드 내 채널 목록이 하드코딩되어 있음 (블로그, Brevo, getlate 등). 동적 관리 불가
- 승인 플로우 미구현:
content_queue.status에 승인 관련 상태('reviewing', 'approved')가 없음 - 2-Layer 상태 추적 불가: 내부 DB 상태와 외부 플랫폼 등록 상태를 분리 추적할 수 없음
- 에러 추적 산재: 에러가 각 테이블의
error_message에 흩어져 있어 통합 모니터링 불가 - 성과 기반 최적화 이력 부재: AI 생성 파라미터 변경 이력과 그 효과를 추적할 수 없음
- 빈 테이블 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 | 기본값 | 설명 | 예시값 |
|---|---|---|---|---|---|
| id | TEXT PK | NO | UUID auto | 채널 고유 ID | "ch-001" |
| name | TEXT | NO | — | 채널 표시명 | "AppPro 블로그" |
| type | TEXT | NO | — | 채널 유형: 'blog' / 'newsletter' / 'sns' / 'external' | "blog" |
| platform | TEXT | NO | — | 플랫폼 식별자 | "apppro.kr" |
| project | TEXT | YES | NULL | 소속 프로젝트 (NULL이면 공통) | "apppro" |
| config | TEXT | YES | NULL | JSON 문자열: API URL, 발행 빈도, 시간대, 콘텐츠 형식 등 | '{"publish_url":"/api/cron/publish","timezone":"Asia/Seoul","format":"markdown"}' |
| credentials_ref | TEXT | YES | NULL | 환경 변수 키 참조 (실제 값 저장 금지) | "BREVO_API_KEY" |
| is_active | INTEGER | NO | 1 | 채널 활성 여부 (0=비활성, 1=활성) | 1 |
| created_at | INTEGER | NO | unixepoch()*1000 | 생성 시각 (ms epoch) | 1740000000000 |
| updated_at | INTEGER | NO | unixepoch()*1000 | 수정 시각 (ms epoch) | 1740000000000 |
인덱스:
idx_channels_type—type: 채널 유형별 목록 조회idx_channels_project—project: 프로젝트별 채널 필터링idx_channels_active—is_active: 활성 채널만 조회
관계: content_distributions.channel_id → channels.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):
| name | type | platform | project |
|---|---|---|---|
| AppPro 블로그 | blog | apppro.kr | apppro |
| Brevo 뉴스레터 | newsletter | brevo | apppro |
| Twitter/X | sns | NULL (공통) |
2-2. content_distributions (채널별 배포 추적, 2-Layer 상태)
목적: 기획서 5-3의 핵심 요구사항인 "2-Layer 상태 관리"를 구현한다. 하나의 콘텐츠가 여러 채널에 배포될 때, 각 채널별 등록/발행 상태를 독립적으로 추적한다. 내부 DB 상태(content_queue.status)와 외부 플랫폼 상태(platform_status)를 분리한다.
사용 시나리오:
- 콘텐츠 승인(approved) 후 블로그/Brevo/Twitter 3개 채널에 동시 예약 등록
- 채널별로 등록 성공/실패/발행 완료를 독립 추적
- 대시보드에서 "3/3 채널 등록 완료" 같은 통합 현황 표시
| 컬럼 | 타입 | NULL | 기본값 | 설명 | 예시값 |
|---|---|---|---|---|---|
| id | TEXT PK | NO | UUID auto | 배포 고유 ID | "dist-001" |
| content_id | TEXT | NO | — | content_queue.id 참조 | "cq-001" |
| channel_id | TEXT | NO | — | channels.id 참조 | "ch-001" |
| platform_status | TEXT | NO | 'pending' | 'pending' / 'registered' / 'published' / 'failed' | "registered" |
| platform_id | TEXT | YES | NULL | 외부 플랫폼 ID (campaign_id, post_id 등) | "campaign_abc123" |
| platform_url | TEXT | YES | NULL | 발행된 URL | "https://blog.apppro.kr/posts/ai-trend-2026" |
| scheduled_at | INTEGER | YES | NULL | 예약 발행 시간 (ms epoch) | 1740100000000 |
| published_at | INTEGER | YES | NULL | 실제 발행 시간 (ms epoch) | 1740100060000 |
| error_message | TEXT | YES | NULL | 실패 시 에러 메시지 | "Brevo API 401 Unauthorized" |
| retry_count | INTEGER | NO | 0 | 자동 재시도 횟수 | 2 |
| created_at | INTEGER | NO | unixepoch()*1000 | 등록 시각 | 1740000000000 |
| updated_at | INTEGER | NO | unixepoch()*1000 | 수정 시각 | 1740000000000 |
인덱스:
idx_distributions_content—content_id: 콘텐츠별 배포 현황 조회 (대시보드 핵심 쿼리)idx_distributions_channel—channel_id: 채널별 배포 통계idx_distributions_status—platform_status: 실패/대기 건 필터링idx_distributions_scheduled—scheduled_at: 발행 예정 스케줄 조회
관계:
content_id→content_queue.id(논리적 FK)channel_id→channels.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 | 기본값 | 설명 | 예시값 |
|---|---|---|---|---|---|
| id | TEXT PK | NO | UUID auto | 에러 로그 ID | "err-001" |
| occurred_at | INTEGER | NO | unixepoch()*1000 | 에러 발생 시각 | 1740000000000 |
| component | TEXT | NO | — | 발생 컴포넌트 | "rss_collector" |
| error_type | TEXT | NO | — | 에러 유형 | "timeout" |
| error_message | TEXT | NO | — | 원본 에러 메시지 | "HTTP 503 Service Unavailable" |
| content_id | TEXT | YES | NULL | 관련 콘텐츠 ID | "cq-001" |
| channel_id | TEXT | YES | NULL | 관련 채널 ID | "ch-002" |
| auto_fix_attempted | INTEGER | NO | 0 | 자동 교정 시도 여부 (0/1) | 1 |
| auto_fix_result | TEXT | YES | NULL | 'success' / 'failed' / 'skipped' | "success" |
| auto_fix_action | TEXT | YES | NULL | 교정 액션 설명 | "RSS URL 자동 교체: old.xml → new.xml" |
| escalated | INTEGER | NO | 0 | 에스컬레이션 여부 (0/1) | 0 |
| resolved_at | INTEGER | YES | NULL | 해결 시각 (ms epoch) | 1740000060000 |
| resolution_type | TEXT | YES | NULL | 'auto_fixed' / 'manual_fixed' / 'ignored' | "auto_fixed" |
인덱스:
idx_error_logs_occurred—occurred_at: 시간순 조회idx_error_logs_component—component: 컴포넌트별 에러 집계idx_error_logs_type—error_type: 에러 유형별 필터idx_error_logs_unresolved—resolved_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 | 기본값 | 설명 | 예시값 |
|---|---|---|---|---|---|
| id | TEXT PK | NO | UUID auto | 이력 ID | "opt-001" |
| changed_at | INTEGER | NO | unixepoch()*1000 | 변경 시각 | 1740000000000 |
| parameter | TEXT | NO | — | 변경 파라미터명 | "publish_time" |
| old_value | TEXT | YES | NULL | 변경 전 값 | "09:00" |
| new_value | TEXT | NO | — | 변경 후 값 | "08:30" |
| reason | TEXT | NO | — | 변경 근거 (데이터 기반) | "오전 8시대 오픈율 +5% (최근 2주 통계)" |
| impact_measured | INTEGER | NO | 0 | 효과 측정 완료 여부 (0/1) | 1 |
| impact_result | TEXT | YES | NULL | 'improved' / 'degraded' / 'neutral' | "improved" |
| impact_detail | TEXT | YES | NULL | 효과 상세 설명 | "오픈율 32%→37%, +5%p 개선" |
인덱스:
idx_optimization_changed—changed_at: 시간순 조회idx_optimization_param—parameter: 파라미터별 변경 이력 조회
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 | 기본값 | 설명 | 예시값 |
|---|---|---|---|---|---|
| id | TEXT PK | NO | UUID auto | 설정 ID | "cfg-001" |
| content_type | TEXT | NO | — | 'blog' / 'newsletter' / 'sns' | "blog" |
| project | TEXT | YES | NULL | 프로젝트명 (NULL이면 전역 기본값) | "apppro" |
| model | TEXT | NO | 'gemini-flash' | AI 모델 식별자 | "gemini-2.0-flash" |
| temperature | REAL | NO | 0.7 | 생성 온도 | 0.7 |
| max_tokens | INTEGER | NO | 2000 | 최대 토큰 수 | 2000 |
| prompt_template | TEXT | YES | NULL | 프롬프트 템플릿 (길 경우 TEXT) | "당신은 AI 전문 기자입니다..." |
| style_guide | TEXT | YES | NULL | 스타일 가이드 (톤, 어투, 형식) | '{"tone":"professional","length":"1500-2000자","language":"ko"}' |
| qa_threshold | REAL | NO | 0.7 | QA 통과 임계값 (0.0~1.0) | 0.7 |
| is_active | INTEGER | NO | 1 | 활성 여부 (0/1) | 1 |
| created_at | INTEGER | NO | unixepoch()*1000 | 생성 시각 | 1740000000000 |
| updated_at | INTEGER | NO | unixepoch()*1000 | 수정 시각 | 1740000000000 |
인덱스:
idx_gen_config_type_project—content_type, project: 유형+프로젝트별 설정 조회 (핵심 쿼리)idx_gen_config_active—is_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 값: draft → reviewing → approved → scheduled → published → failed
| 변경 | 컬럼 | 타입 | 설명 |
|---|---|---|---|
| ADD COLUMN | title | TEXT | 콘텐츠 제목 (대시보드 표시용) |
| ADD COLUMN | content_body | TEXT | 콘텐츠 본문 (승인 전 미리보기용) |
| ADD COLUMN | approved_by | TEXT | 승인자 ('ceo' / 'auto') |
| ADD COLUMN | approved_at | INTEGER | 승인 시각 (ms epoch) |
| ADD COLUMN | rejected_reason | TEXT | 거부 사유 (피드백 루프) |
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 COLUMN | error_log_id | TEXT | 실패 시 error_logs.id 참조 |
| ADD COLUMN | trigger_type | TEXT | 'manual' / 'scheduled' / 'retry' (실행 유형) |
| ADD COLUMN | parent_log_id | TEXT | 재시도인 경우, 원본 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 COLUMN | content_queue_id | TEXT | 원본 content_queue.id 참조 |
| ADD COLUMN | project | TEXT | 소속 프로젝트 |
마이그레이션 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. 안전 장치
- 모든 CREATE는 IF NOT EXISTS: 중복 실행 안전
- ALTER TABLE은 .catch(() => {}): 컬럼 이미 존재 시 무시 (기존 content-db.ts 패턴 유지)
- 실행 전 백업 필수:
turso db shell content-os ".dump" > backup-{날짜}.sql - 트랜잭션 권장: 가능하면 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주) — 필수 테이블
| 순서 | 테이블 | 작업 | 이유 |
|---|---|---|---|
| 1 | channels | 신규 생성 + 시드 | 채널 관리 기초, 배포 대상 정의 |
| 2 | content_queue 확장 | ALTER 5개 컬럼 | 승인 플로우의 핵심 |
| 3 | content_distributions | 신규 생성 | 2-Layer 상태 추적 (MVP 완료 기준) |
| 4 | newsletters 확장 | ALTER 2개 컬럼 | content_queue 연계 |
| 5 | error_logs | 신규 생성 | 에러 기록 기초 (L1 자동 재시도용) |
Phase 2 (2주) — 확장 테이블
| 순서 | 테이블 | 작업 | 이유 |
|---|---|---|---|
| 6 | content_generation_config | 신규 생성 | AI 파라미터 동적 관리 |
| 7 | pipeline_logs 확장 | ALTER 3개 컬럼 | 자체교정 연계 |
| 8 | optimization_history | 신규 생성 | 성과 기반 최적화 이력 |
Phase 3 (3주+) — 데이터 고도화
channels: 외부 플랫폼 채널 추가 (티스토리, 미디엄, EO 등)content_generation_config: 프로젝트별/채널별 세분화 설정optimization_history: 자동 A/B 테스트 이력 추가content_distributions: 성과 metrics 컬럼 추가 (views, clicks, conversions)
7. 전체 테이블 요약
최종 테이블 구성 (Phase 1 완료 후)
| # | 테이블 | 상태 | 역할 |
|---|---|---|---|
| 1 | collected_news | 기존 유지 | RSS 수집 원본 저장 |
| 2 | newsletters | 기존 + 확장 | 뉴스레터 콘텐츠 + 큐 연계 |
| 3 | content_queue | 기존 + 확장 | 승인 플로우 중심 콘텐츠 관리 |
| 4 | content_logs | 기존 유지 | 콘텐츠 발행 이벤트 기록 |
| 5 | pipeline_logs | 기존 유지 (Phase 2에서 확장) | 파이프라인 실행 로그 |
| 6 | plf_schedule | 기존 유지 | PLF 런칭 스케줄 |
| 7 | channels | 신규 | 채널/계정/매체 동적 관리 |
| 8 | content_distributions | 신규 | 채널별 배포 추적, 2-Layer 상태 |
| 9 | error_logs | 신규 | 통합 에러 로그, 자체교정 |
| 10 | content_generation_config | 신규 (Phase 2) | AI 생성 파라미터 관리 |
| 11 | optimization_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 보고 대기