← 목록으로
2026-02-25plans

title: content-orchestration 외부 연동 설계서 (L1) date: 2026-02-25T16:07:00+09:00 type: design layer: L1 status: in-review tags: [content-orchestration, external-integration, L1, brevo, getlate, telegram] author: ext-design-pl project: content-orchestration reviewed_by: "jarvis" reviewed_at: "2026-02-25T18:50:00+09:00" approved_by: "" approved_at: ""

content-orchestration 외부 연동 설계서 (L1)

Task ID: dc1d0d04-5bca-4d69-abc7-e0b4f3e40c79 작성일: 2026-02-25 작성자: ext-design-pl 근거 문서:

  • L0: content-orchestration-biz-v4.md (비즈 기획서)
  • L1: content-orchestration-design-db.md (DB 설계서, 검수 완료)
  • L1: content-orchestration-design-pipeline.md (파이프라인 설계서, 승인)

1. 현황 분석

1-1. 현재 외부 연동 코드 현황

content-pipeline 프로젝트(projects/content-pipeline/src/)에 이미 구현된 외부 연동:

연동 대상코드 파일라이브러리현재 상태
Brevo (이메일)lib/brevo.ts@getbrevo/brevo (BrevoClient)구현 완료. mock/live 모드 분기. API 키 설정 시 캠페인 생성+즉시발송 동작
getlate.dev (SNS)lib/getlate.ts, pipeline/publish-sns.tsfetch (REST API 직접)구현 완료. 계정 조회, 멀티플랫폼 발행, 예약 발행 지원
AppPro 블로그 DBpipeline/publish-blog.ts, pipeline/publish.ts@libsql/client/web (Turso)구현 완료. blog_posts 테이블 INSERT, slug 중복 검사
Telegram Bot미구현자비스 스크립트(vice-reply.sh, ceo-reply.sh)로 알림. 파이프라인에서 직접 호출 없음

1-2. 환경 변수 현황

현재 .env에 설정된 외부 연동 관련 키:

환경 변수용도설정 상태
BREVO_API_KEYBrevo 이메일 캠페인 API 인증설정됨
BREVO_LIST_ID기본 발송 대상 리스트 ID (테스트 그룹 ID=8)설정됨
GETLATE_API_KEYgetlate.dev SNS 배포 API 인증설정됨
CONTENT_OS_DB_URLcontent-os Turso DB URL (뉴스레터/수집 데이터)설정됨
CONTENT_OS_DB_TOKENcontent-os Turso 인증 토큰설정됨
TURSO_DB_URLapppro-kr Turso DB URL (블로그 포스트)설정됨
TURSO_DB_TOKENapppro-kr Turso 인증 토큰설정됨
CODEX_VP_BOT_TOKENTelegram Bot 토큰 (VP 보고용)설정됨
CODEX_VP_CHAT_IDTelegram Chat ID (VP 채팅)설정됨

1-3. 현재 한계점

#한계상세설계서 해결 방안
1Brevo 예약 발송 미구현sendCampaignNow만 사용. scheduledAt 파라미터 미활용캠페인 생성 시 scheduledAt 예약 발송 추가
2Brevo 캠페인 상태 조회 미구현발송 후 성공/실패/오픈율 추적 불가getCampaign API로 상태 폴링
3getlate 에러 처리 미흡NO_ACCOUNTS 외 에러는 일반 로그만error_logs 연계 + 재시도 전략
4연동 결과가 DB에 미기록발송/배포 결과가 content_distributions에 저장되지 않음모든 연동 결과를 content_distributions에 기록
5인증 갱신 자동화 없음API 키 만료/변경 시 수동 .env 수정 필요channels.credentials_ref로 환경 변수 키 참조, 인증 실패 시 에스컬레이션
6Telegram 파이프라인 알림 없음파이프라인 내 승인 요청/에러 알림을 Telegram으로 전송하는 로직 없음Phase 2에서 Telegram Bot API 연동
7블로그 발행 확인 메커니즘 없음blog_posts에 INSERT 후 실제 발행 여부 확인 불가Vercel Cron 발행 후 published=1 확인 폴링

2. 연동 대상별 상세 설계

2-1. Brevo API (뉴스레터 발송) — Phase 1

개요

항목상세
서비스Brevo (구 SendinBlue) — 이메일 마케팅 플랫폼
SDK@getbrevo/brevo (BrevoClient 클래스)
용도뉴스레터 캠페인 생성, 예약 발송, 구독자 관리
현재 코드lib/brevo.ts — addContact, createList, sendCampaign, getBrevoStatus
PhasePhase 1 (MVP)

API 엔드포인트 명세

1) 캠페인 생성 + 예약 발송

항목상세
SDK 메서드client.emailCampaigns.createEmailCampaign()
대응 RESTPOST /emailCampaigns
인증api-key 헤더 (BREVO_API_KEY)

요청 파라미터:

파라미터타입필수설명
namestringY캠페인 이름 (내부 식별용)
subjectstringY이메일 제목
htmlContentstringYHTML 본문
senderobjectY{name: "AI AppPro", email: "hello@apppro.kr"}
recipientsobjectY{listIds: [BREVO_LIST_ID]}
scheduledAtstringN예약 발송 시각 (ISO 8601, 예: 2026-02-26T10:00:00+09:00)

응답:

{
  "id": 12345
}

2) 캠페인 즉시 발송

항목상세
SDK 메서드client.emailCampaigns.sendEmailCampaignNow({campaignId})
대응 RESTPOST /emailCampaigns/{campaignId}/sendNow
조건scheduledAt 미설정 시에만 사용

3) 캠페인 상태 조회 (신규 구현)

항목상세
SDK 메서드client.emailCampaigns.getEmailCampaign({campaignId})
대응 RESTGET /emailCampaigns/{campaignId}

응답 주요 필드:

필드설명
statusdraft / queued / sent / archive
statistics.globalStats.delivered발송 성공 수
statistics.globalStats.opens오픈 수
statistics.globalStats.clicks클릭 수
statistics.globalStats.bounces반송 수

4) 구독자 추가

항목상세
SDK 메서드client.contacts.createContact()
대응 RESTPOST /contacts
현재 코드addContact() — 이미 구현 완료

인증 방식

항목상세
방식API Key (헤더 인증)
헤더api-key: {BREVO_API_KEY}
환경 변수BREVO_API_KEY
SDK 초기화new BrevoClient({ apiKey: process.env.BREVO_API_KEY })
만료API 키는 영구. Brevo 대시보드에서 수동 폐기/재발급
보안channels 테이블 credentials_ref = "BREVO_API_KEY" (키 이름만 저장, 실제 값은 Vercel env)

Rate Limit

제한대응 전략
Free Plan 일일 발송 한도300통/일일 300통 초과 시 → 다음 날로 자동 지연 예약
API 호출 한도없음 (Brevo Free는 API 호출 자체는 무제한)
HTTP 429 (과다 요청)발생 가능 (연속 빠른 호출 시)60초 대기 후 1회 재시도 → 실패 시 error_logs
캠페인 생성 한도Free Plan 기준 명시적 제한 없음

Fallback 처리

실패 유형HTTP 코드Fallback 행동
인증 실패401error_logs(error_type='auth_fail', escalated=1) → CEO에게 키 갱신 요청
리스트 미존재404error_logs → BREVO_LIST_ID 확인 에스컬레이션
발송 한도 초과402/429scheduledAt을 다음 날 06:00 KST로 변경하여 재예약
캠페인 생성 실패400error_logs(error_type='validation_fail') → 요청 파라미터 검증 후 재시도
네트워크 에러timeout30초 후 1회 재시도 → 실패 시 error_logs
mock 모드BREVO_API_KEY 미설정 시 → console.log만 출력, content_distributions.platform_status='failed', error_message='MOCK_MODE'

DB 연계

테이블연계 방식
channelsid='ch-brevo', type='newsletter', platform='brevo', credentials_ref='BREVO_API_KEY', config='{"list_id":8,"template":"weekly"}'
content_distributions캠페인 생성 성공 시 → platform_status='registered', platform_id=campaign_id, scheduled_at=예약시각. 발송 완료 확인 후 → platform_status='published', published_at=발송시각
error_logs실패 시 → component='brevo', error_type, error_message, channel_id='ch-brevo'
newslettersemail_service_id에 Brevo campaign_id 저장 (기존 컬럼 활용)

발송 플로우

content_queue.status = 'approved'
    │
    ↓
content_distributions INSERT (channel_id='ch-brevo', platform_status='pending')
    │
    ↓
newsletters 테이블에서 HTML 콘텐츠 조회
    │
    ↓
Brevo createEmailCampaign(scheduledAt 포함)
    │
    ├─ 성공: content_distributions UPDATE
    │        platform_status='registered'
    │        platform_id={campaign_id}
    │        scheduled_at={예약시각}
    │
    └─ 실패: content_distributions UPDATE platform_status='failed'
             error_logs INSERT
    │
    ↓ (예약 시각 도래 후 Brevo 자체 발송)
    │
Brevo 캠페인 상태 폴링 (getCampaign)
    │
    ├─ status='sent': content_distributions UPDATE
    │                  platform_status='published'
    │                  published_at={실제발송시각}
    │
    └─ status='failed': error_logs INSERT + 에스컬레이션

2-2. AppPro.kr 블로그 DB (Turso 직접 INSERT) — Phase 1

개요

항목상세
서비스apppro.kr 블로그 (자체 운영)
DBTurso (LibSQL) — apppro-kr DB
테이블blog_posts
라이브러리@libsql/client/web
용도AI 생성 블로그 포스트를 DB에 저장하여 자동 발행 (Vercel Cron 연계)
현재 코드pipeline/publish-blog.ts — publishBlogPost()
PhasePhase 1 (MVP)

연동 방식 (API가 아닌 DB 직접 접근)

블로그는 외부 API가 아니라 apppro-kr Turso DB에 직접 INSERT한다. 기존 Vercel Cron(/api/cron/publish, 6시간 주기)이 scheduled_at 시간이 도래한 포스트를 published=1로 전환하여 자동 발행한다.

INSERT SQL:

INSERT INTO blog_posts (
  id, title, slug, content, excerpt, category, tags,
  author, published, publishedAt, metaDescription, createdAt, updatedAt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, ?)
  • published=0: 예약 상태 (Cron이 시간 되면 1로 전환)
  • publishedAt=NULL: Cron이 발행 시 현재 시각으로 설정

인증 방식

항목상세
방식Turso Auth Token (Bearer Token)
환경 변수TURSO_DB_URL, TURSO_DB_TOKEN
SDKcreateClient({ url, authToken })
만료Turso 토큰은 대시보드에서 관리. 명시적 만료 기간 없음
보안channels 테이블 credentials_ref = "TURSO_DB_TOKEN"

Rate Limit

제한대응 전략
Turso Free Plan reads500M reads/월현재 사용량 극소. 문제 없음
Turso Free Plan writes10M writes/월블로그 포스트 수준에서 문제 없음
Turso Free Plan storage5GB텍스트 포스트 기준 수만 건 가능
동시 연결제한 없음 (HTTP 기반)

Fallback 처리

실패 유형Fallback 행동
slug 중복slug에 날짜+순번 자동 추가 ({slug}-2026-02-25-2)
DB 연결 실패1회 재시도 (5초 대기) → 실패 시 error_logs
인증 토큰 만료error_logs(error_type='auth_fail', escalated=1) → CEO에게 토큰 갱신 요청
데이터 유효성 오류error_logs(error_type='validation_fail') → 콘텐츠 검증 후 재시도

DB 연계

테이블연계 방식
channelsid='ch-apppro-blog', type='blog', platform='apppro.kr', credentials_ref='TURSO_DB_TOKEN', config='{"publish_api":"/api/cron/publish","auto_publish":true}'
content_distributionsINSERT 성공 시 → platform_status='registered', platform_id={blog_post_id}, scheduled_at. Vercel Cron 발행 확인 후 → platform_status='published', platform_url=https://apppro.kr/blog/posts/{slug}
error_logs실패 시 → component='publisher', error_type, channel_id='ch-apppro-blog'

발행 플로우

content_queue.status = 'approved'
    │
    ↓
content_distributions INSERT (channel_id='ch-apppro-blog', platform_status='pending')
    │
    ↓
content_queue에서 title, content_body, 메타데이터 조회
    │
    ↓
apppro-kr Turso DB blog_posts INSERT (published=0)
    │
    ├─ 성공: content_distributions UPDATE
    │        platform_status='registered'
    │        platform_id={blog_post_id}
    │        scheduled_at={발행예약시각}
    │
    └─ 실패 (slug 중복): slug 변경 후 재시도
    └─ 실패 (DB 에러): error_logs INSERT
    │
    ↓ (Vercel Cron /api/cron/publish, 6시간 주기)
    │
blog_posts.published = 1, publishedAt = now
    │
    ↓ (발행 확인 폴링 — Phase 1)
    │
content_distributions UPDATE
    platform_status='published'
    platform_url='https://apppro.kr/blog/posts/{slug}'
    published_at={실제발행시각}

발행 확인 메커니즘 (신규)

Vercel Cron이 발행한 뒤 content_distributions를 갱신하기 위한 확인 로직:

content_distributions에서 platform_status='registered' AND channel_id='ch-apppro-blog' 조회
    │
    ↓
apppro-kr DB에서 해당 blog_post_id로 published 컬럼 확인
    │
    ├─ published=1: platform_status='published', published_at 갱신
    └─ published=0: 아직 대기 중 (다음 폴링에서 재확인)
  • 확인 주기: Vercel Cron /api/cron/pipeline 실행 시 함께 체크 (1일 1회)
  • 또는 별도 /api/cron/check-publish 엔드포인트 (Phase 2)

2-3. getlate.dev API (SNS 멀티플랫폼) — Phase 1~2

개요

항목상세
서비스getlate.dev (Late) — SNS 멀티플랫폼 예약 발행 SaaS
APIREST API v1 (https://getlate.dev/api/v1)
라이브러리fetch (직접 호출)
지원 플랫폼X(Twitter), LinkedIn, Threads, Instagram, Facebook, TikTok, YouTube, Reddit, Pinterest, Bluesky, Telegram, Snapchat, Google Business
용도블로그 포스트 홍보, 뉴스레터 알림을 SNS에 자동 배포
현재 코드lib/getlate.ts, pipeline/publish-sns.ts
PhasePhase 1 (Twitter/X, LinkedIn), Phase 2 (Threads, Instagram)

API 엔드포인트 명세

1) 계정 조회

항목상세
메서드GET /accounts
인증Authorization: Bearer {GETLATE_API_KEY}

응답:

{
  "data": [
    {
      "accountId": "abc123",
      "platform": "twitter",
      "name": "AI AppPro",
      "username": "@apppro_ai"
    }
  ]
}

2) 포스트 생성 (즉시/예약)

항목상세
메서드POST /posts
인증Authorization: Bearer {GETLATE_API_KEY}

요청 파라미터:

파라미터타입필수설명
contentstringY기본 콘텐츠 텍스트
platformsarrayY대상 플랫폼 배열: [{platform, accountId, customContent?}]
publishNowbooleanNtrue면 즉시 발행 (scheduledFor 무시)
scheduledForstringN예약 시각 (ISO 8601: 2026-02-26T12:00:00)
timezonestringN타임존 (기본: Asia/Seoul)
mediaUrlsarrayN이미지/미디어 URL 목록

응답:

{
  "data": {
    "id": "post_xyz789",
    "content": "...",
    "status": "scheduled",
    "scheduledFor": "2026-02-26T12:00:00+09:00",
    "platforms": [
      {"platform": "twitter", "accountId": "abc123", "status": "scheduled"},
      {"platform": "linkedin", "accountId": "def456", "status": "scheduled"}
    ],
    "createdAt": "2026-02-25T16:00:00+09:00"
  }
}

3) 포스트 상태 조회 (Phase 2)

항목상세
메서드GET /posts/{postId}
인증Authorization: Bearer {GETLATE_API_KEY}

인증 방식

항목상세
방식Bearer Token (API Key)
헤더Authorization: Bearer {GETLATE_API_KEY}
환경 변수GETLATE_API_KEY
발급getlate.dev 대시보드 > API Keys
보안channels 테이블 credentials_ref = "GETLATE_API_KEY"

Rate Limit

제한대응 전략
API 호출 한도문서 미명시 (일반적으로 분당 60회 정도)연속 호출 간 1초 딜레이
일일 포스트 한도플랜별 상이 (Free: 30포스트/월)월간 한도 추적, 초과 시 다음 월로 지연
HTTP 429과다 요청 시 발생 가능60초 대기 후 재시도

플랫폼별 콘텐츠 변환 규칙

기존 publish-sns.tsconvertToSnsContent() 함수에 정의된 규칙:

플랫폼글자 제한변환 규칙
Twitter/X280자{제목}\n\n{블로그URL}\n\n{해시태그 5개} — 초과 시 해시태그 제거
Bluesky300자Twitter와 동일
LinkedIn3,000자{제목}\n\n{요약 전문}\n\n{블로그URL}\n\n{해시태그}
Threads500자{제목}\n\n{요약}\n\n{블로그URL}\n\n{해시태그} — 초과 시 해시태그 제거
Instagram2,200자Threads와 동일 포맷
Facebook2,000자기본 포맷: {제목}\n\n{요약}\n\n{URL}\n\n{해시태그}

Fallback 처리

실패 유형Fallback 행동
API 키 미설정mock 모드 → console.log만 출력. content_distributions.platform_status='failed', error_message='MOCK_MODE'
계정 미연결 (NO_ACCOUNTS)error_logs(error_type='auth_fail') → "getlate.dev에서 SNS 계정 연결 필요" 에스컬레이션
특정 플랫폼 계정 없음해당 플랫폼만 skip, 나머지 플랫폼 정상 진행
API 오류 (4xx/5xx)1회 재시도 (30초 대기) → 실패 시 error_logs
네트워크 타임아웃1회 재시도 → 실패 시 해당 플랫폼 skip + error_logs
일부 플랫폼만 실패성공 플랫폼은 registered, 실패 플랫폼은 failed. 각각 별도 content_distributions 레코드

DB 연계

테이블연계 방식
channelsSNS 플랫폼별 별도 레코드: id='ch-twitter', id='ch-linkedin' 등. type='sns', credentials_ref='GETLATE_API_KEY'
content_distributions플랫폼별 별도 레코드 생성. channel_id='ch-twitter', platform_id={getlate_post_id}. 발행 확인 후 platform_url={플랫폼별 URL}
error_logscomponent='sns_publisher', channel_id에 해당 채널 ID

발행 플로우

content_queue.status = 'approved'
    │
    ↓
channels 테이블에서 type='sns' AND is_active=1 조회
    │
    ↓
getlate GET /accounts → 연결된 계정 확인
    │
    ↓
플랫폼별 콘텐츠 변환 (convertToSnsContent)
    │
    ↓
채널별 content_distributions INSERT (platform_status='pending')
    │
    ↓
getlate POST /posts (scheduledFor 포함, platforms 배열)
    │
    ├─ 성공: 플랫폼별 content_distributions UPDATE
    │        platform_status='registered'
    │        platform_id={getlate_post_id}
    │        scheduled_at={예약시각}
    │
    └─ 부분 실패: 성공 플랫폼은 registered, 실패 플랫폼은 failed
                  error_logs INSERT (실패 플랫폼별)
    │
    ↓ (예약 시각 도래 후 getlate 자체 발행)
    │
발행 확인 (Phase 2: GET /posts/{id} 폴링)
    │
    ↓
content_distributions UPDATE
    platform_status='published'
    platform_url={각 플랫폼 포스트 URL}
    published_at={실제발행시각}

2-4. Telegram Bot API (관리자 알림) — Phase 2

개요

항목상세
서비스Telegram Bot API
용도CEO에게 승인 요청 알림, 에러 에스컬레이션 알림
현재 상태vice-reply.sh, ceo-reply.sh 스크립트로 보고. 파이프라인 직접 연동은 없음
PhasePhase 2

연동 범위 (Phase 2)

Telegram 연동은 기존 보고 스크립트를 활용하되, 파이프라인에서 특정 이벤트 발생 시 자동 알림을 추가한다.

알림 유형:

#이벤트알림 내용방법
1콘텐츠 검수 요청content_queue reviewing 전환 시 CEO에게 미리보기 링크 전송ceo-reply.sh를 API에서 호출
2에러 에스컬레이션error_logs.escalated=1일 때 에러 요약 전송vice-reply.sh를 API에서 호출
3일일 파이프라인 리포트당일 수집/생성/발행 건수 요약vice-reply.sh (Cron에서 호출)
4발송 완료 알림Brevo/SNS 발행 완료 시 결과 요약vice-reply.sh

구현 방식

기존 스크립트를 shell 명령으로 호출하는 방식을 유지한다. Telegram API를 직접 호출하지 않는다 (CLAUDE.md 규칙: "Telegram API 직접 호출 / secrets 접근 절대 금지").

파이프라인 이벤트 발생
    │
    ├─ CEO 검수 요청: exec(`ceo-reply.sh "🤖 [자비스] 콘텐츠 검수 요청\n제목: {title}\n미리보기: {url}" "vice-claude"`)
    │
    ├─ 에러 에스컬레이션: exec(`vice-reply.sh "🤖 [자비스] 파이프라인 에러\n컴포넌트: {component}\n에러: {message}" "error"`)
    │
    └─ 발행 완료: exec(`vice-reply.sh "🤖 [자비스] 콘텐츠 발행 완료\n제목: {title}\n채널: {channels}" "publish"`)

인증 방식

항목상세
방식기존 스크립트 (vice-reply.sh, ceo-reply.sh) 통해 간접 호출
환경 변수CODEX_VP_BOT_TOKEN, CODEX_VP_CHAT_ID (스크립트 내부에서 참조)
직접 API 호출금지 (보안 정책)

Rate Limit

제한대응 전략
Telegram Bot API30 메시지/초 (그룹), 1 메시지/초 (개인)알림 간 1초 딜레이. 대량 알림 시 배치
vice-reply.sh자체 제한 없음호출 간 최소 2초 간격 유지

Fallback 처리

실패 유형Fallback 행동
스크립트 실행 실패error_logs 기록. 대시보드에서 알림 내용 확인 가능 (pipeline_logs)
Bot 토큰 만료error_logs(escalated=1) → 대시보드 에러 표시

DB 연계

Telegram은 channels 테이블에 등록하지 않는다 (배포 채널이 아닌 관리 알림용). pipeline_logsmetadata에 알림 전송 결과를 기록한다.


2-5. 외부 플랫폼 (티스토리, 미디엄, EO 등) — Phase 3

개요

Phase 3에서 추가 예정인 외부 콘텐츠 플랫폼 연동. 현재는 설계 개요만 기록하며, 실제 구현 시점에 상세 설계를 작성한다.

플랫폼별 연동 방법 요약

플랫폼API 지원연동 방법인증난이도우선순위
티스토리Open API 있음REST APIOAuth 2.0Phase 3 우선
미디엄API 있음POST /users/{id}/postsBearer Token (Integration Token)Phase 3 우선
LinkedIn 아티클Marketing APIPOST /ugcPostsOAuth 2.0 (3-legged)Phase 3
EO (eopla.net)확인 필요API 또는 Playwright미확인Phase 3
네이버 블로그공식 API 없음Playwright 브라우저 자동화쿠키/세션Phase 3+ (최후순위)
브런치API 없음Playwright 브라우저 자동화쿠키/세션Phase 3+ (최후순위)

Phase 3 연동 원칙

  1. API 우선: API가 있는 플랫폼(티스토리, 미디엄)부터 연동
  2. Playwright는 최후수단: API 없는 플랫폼은 캡차/보안 이슈로 불안정. 최후순위
  3. channels 테이블 확장: 새 플랫폼 추가 시 channels INSERT만으로 활성화
  4. 콘텐츠 변환: 플랫폼별 포맷 변환 로직 추가 (마크다운 → HTML 등)

3. 공통 설계 사항

3-1. channels 테이블 기반 동적 채널 관리

모든 외부 연동은 channels 테이블을 통해 관리한다. 채널 추가/비활성화를 코드 수정 없이 DB 조작만으로 가능하게 한다.

Phase 1 시드 데이터:

idnametypeplatformprojectcredentials_refconfigis_active
ch-apppro-blogAppPro 블로그blogapppro.krappproTURSO_DB_TOKEN{"publish_api":"/api/cron/publish","auto_publish":true}1
ch-brevoBrevo 뉴스레터newsletterbrevoappproBREVO_API_KEY{"list_id":8,"template":"weekly","sender_name":"AI AppPro","sender_email":"hello@apppro.kr"}1
ch-twitterTwitter/XsnstwitterNULLGETLATE_API_KEY{"max_chars":280,"via_getlate":true}0
ch-linkedinLinkedInsnslinkedinNULLGETLATE_API_KEY{"max_chars":3000,"via_getlate":true}0

SNS 채널(ch-twitter, ch-linkedin)은 초기에 is_active=0으로 설정. getlate.dev에서 계정 연결 확인 후 is_active=1로 전환.

3-2. content_distributions 연계 패턴 (공통)

모든 외부 연동은 동일한 패턴으로 content_distributions에 기록한다:

1. 배포 시작: INSERT (content_id, channel_id, platform_status='pending')
2. 외부 등록 성공: UPDATE platform_status='registered', platform_id, scheduled_at
3. 발행 확인: UPDATE platform_status='published', platform_url, published_at
4. 실패: UPDATE platform_status='failed', error_message, retry_count++

상태 전이 규칙:

pending ──→ registered ──→ published
   │              │
   │              └──→ failed (발행 실패)
   │
   └──→ failed (등록 실패)

failed ──→ pending (재시도 시 새 레코드 INSERT)

3-3. error_logs 연계 패턴 (공통)

모든 외부 연동 에러는 동일한 패턴으로 error_logs에 기록한다:

필드채워야 하는 값
component'brevo' / 'publisher' / 'sns_publisher' / 'scheduler'
error_type'timeout' / 'auth_fail' / 'api_error' / 'rate_limit' / 'validation_fail'
error_message원본 에러 메시지 (HTTP 상태 코드 + 응답 본문)
content_id관련 content_queue.id
channel_id관련 channels.id
auto_fix_attempted자동 재시도 여부 (0/1)
auto_fix_result재시도 결과 ('success' / 'failed' / 'skipped')
escalatedCEO/VP 알림 필요 여부 (0/1)

에스컬레이션 기준:

조건escalated
인증 실패 (auth_fail)1 (즉시)
3회 연속 자동 교정 실패1
모든 채널 실패1
단일 채널 실패 (나머지 성공)0

3-4. 환경 변수 → channels.credentials_ref 매핑

channels.credentials_ref 값    →   참조하는 환경 변수
─────────────────────────────────────────────────
"BREVO_API_KEY"               →   process.env.BREVO_API_KEY
"GETLATE_API_KEY"             →   process.env.GETLATE_API_KEY
"TURSO_DB_TOKEN"              →   process.env.TURSO_DB_TOKEN
"TISTORY_ACCESS_TOKEN"        →   process.env.TISTORY_ACCESS_TOKEN (Phase 3)
"MEDIUM_INTEGRATION_TOKEN"    →   process.env.MEDIUM_INTEGRATION_TOKEN (Phase 3)

런타임에서 채널의 인증 정보를 가져오는 로직:

channels 테이블에서 credentials_ref 조회
    → process.env[credentials_ref] 값 참조
    → null이면 해당 채널 skip (mock 모드)

3-5. mock 모드 통일

모든 외부 연동은 API 키 미설정 시 mock 모드로 동작한다:

행동상세
실행 중단 없음파이프라인은 계속 진행
console.logmock 모드임을 로그에 기록
content_distributionsplatform_status='failed', error_message='MOCK_MODE: {credentials_ref} not set'
error_logs기록하지 않음 (mock은 에러가 아님)
대시보드 표시"API 키 미설정" 경고 배지

4. Phase별 구현 시점

Phase 1 (MVP, 1주) — 필수 연동

#연동 대상구현 내용기존 코드 활용
1Brevo 뉴스레터캠페인 생성 + 예약 발송 + content_distributions 기록lib/brevo.ts sendCampaign 확장 (scheduledAt 추가)
2AppPro 블로그 DBblog_posts INSERT + content_distributions 기록 + 발행 확인pipeline/publish-blog.ts 래퍼
3getlate SNS멀티플랫폼 예약 발행 + content_distributions 기록lib/getlate.ts + pipeline/publish-sns.ts 래퍼

Phase 1 제한:

  • Brevo 캠페인 상태 폴링은 수동 확인 (대시보드에서 campaign_id 표시)
  • getlate 발행 확인 폴링 없음 (registered → published 수동 전환)
  • Telegram 알림 없음

Phase 2 (2주) — 알림 + 상태 추적

#연동 대상구현 내용
4Telegram 알림검수 요청, 에러 에스컬레이션, 발행 완료 알림 (스크립트 호출)
5Brevo 상태 폴링getCampaign으로 발송 상태/오픈율/클릭 추적
6getlate 상태 폴링GET /posts/{id}로 발행 결과/URL 추적
7블로그 발행 확인apppro-kr DB 폴링으로 published=1 자동 확인

Phase 3 (3주+) — 외부 플랫폼 확장

#연동 대상구현 내용
8티스토리Open API OAuth + 블로그 포스트 자동 발행
9미디엄Integration Token + 포스트 발행 (draft 상태)
10LinkedIn 아티클Marketing API로 긴 형식 아티클 발행
11EO/기타API 조사 후 결정

5. 연동 코드 구조 (content-orchestration 레포)

content-orchestration/
└── src/
    └── lib/
        ├── integrations/
        │   ├── brevo.ts           ← Brevo 연동 래퍼 (content_distributions 연계)
        │   ├── blog-publisher.ts  ← 블로그 DB INSERT 래퍼
        │   ├── sns-publisher.ts   ← getlate SNS 래퍼
        │   ├── telegram.ts        ← Telegram 알림 (스크립트 호출, Phase 2)
        │   └── types.ts           ← 공통 인터페이스 (PublishResult, ChannelConfig 등)
        ├── pipeline/
        │   └── publish.ts         ← 통합 배포 오케스트레이터 (channels → integrations 호출)
        └── db/
            └── content-os.ts      ← Turso 클라이언트 (기존)

통합 배포 오케스트레이터 흐름:

publishContent(contentId: string)
    │
    ├─ channels 테이블에서 is_active=1 채널 목록 조회
    │
    ├─ 채널별 content_distributions INSERT (pending)
    │
    ├─ 채널 type별 분기:
    │   ├─ type='blog' → blog-publisher.ts
    │   ├─ type='newsletter' → brevo.ts
    │   └─ type='sns' → sns-publisher.ts
    │
    ├─ 결과별 content_distributions UPDATE
    │
    ├─ 전체 결과 pipeline_logs 기록
    │
    └─ 에러 시 error_logs 기록 + 필요 시 에스컬레이션

6. 전체 연동 요약 매트릭스

연동 대상Phase인증 방식Rate Limit 핵심Fallback 핵심DB 연계 (channels.id)
Brevo1API Key (헤더)300통/일 (Free)예약 지연 + error_logsch-brevo
블로그 DB1Turso Token500M reads/월slug 변경 재시도ch-apppro-blog
getlate (SNS)1~2Bearer Token플랜별 포스트 한도플랫폼별 skipch-twitter, ch-linkedin
Telegram2스크립트 간접1 msg/초error_logs 기록(미등록)
티스토리3OAuth 2.0미확인error_logs(Phase 3 추가)
미디엄3Bearer Token미확인draft 상태 발행(Phase 3 추가)

리뷰 로그

[ext-design-pl 초안 작성] 2026-02-25 16:07

  • 현재 외부 연동 코드 4개 파일 분석 완료 (publish.ts, publish-blog.ts, publish-sns.ts, lib/brevo.ts, lib/getlate.ts)
  • 환경 변수 현황 파악 (10개 외부 연동 관련 키)
  • 한계점 7건 정리
  • Phase 1 연동 3건 상세 설계: Brevo API (캠페인 생성+예약+상태조회), 블로그 DB (Turso INSERT+발행확인), getlate.dev (멀티플랫폼 예약+플랫폼별 변환)
  • Phase 2 연동 1건 설계: Telegram Bot (스크립트 간접 호출, API 직접 호출 금지)
  • Phase 3 연동 개요: 티스토리(Open API), 미디엄(API), 네이버(Playwright, 최후순위)
  • 공통 설계: channels 기반 동적 관리, content_distributions 연계 패턴, error_logs 패턴, mock 모드 통일, credentials_ref 매핑
  • 연동 코드 구조: integrations/ 디렉토리 기반 래퍼 패턴
  • 자비스 1차 검수 요청

[자비스 1차 검수] 2026-02-25 18:50

검수 결과: 수정 없이 승인 ✅

검수 항목:

  1. 현황 분석 완료 — 기존 4개 연동 파일 전수 분석, 환경 변수 10개 현황, 한계점 7건 정리
  2. Phase 분리 명확 — Phase 1(Brevo/블로그/getlate), Phase 2(Telegram/상태폴링), Phase 3(티스토리/미디엄/네이버)
  3. Telegram 보안 정책 준수 — API 직접 호출 금지, vice-reply.sh/ceo-reply.sh 간접 호출로 설계
  4. mock 모드 통일 — API 키 미설정 시 에러 없이 MOCK_MODE 기록, 파이프라인 중단 없음
  5. channels 기반 동적 관리 — credentials_ref 패턴으로 코드 수정 없이 채널 추가/비활성화
  6. content_distributions 패턴 통일 — pending→registered→published 상태 전이 일관성
  7. error_logs 에스컬레이션 기준 명확 — auth_fail 즉시, 3회 연속 실패 시 에스컬레이션
  8. Phase 1 시드 데이터 정의 — ch-apppro-blog, ch-brevo(active), ch-twitter/linkedin(inactive 초기값)
  9. Fallback 처리 상세 — Brevo 발송한도 초과 시 다음날 재예약, slug 중복 자동 해결
  10. env vars 현황 — CONTENT_OS_DB_URL/TOKEN 포함 모두 .env에 설정됨 확인

추가 확인 사항: env 파일에서 CONTENT_OS_DB_URL/TOKEN 설정 완료 확인 → CEO 블로킹 없음

plans/2026/02/25/content-orchestration-design-external.md