← 목록으로
2026-02-25plans

title: content-orchestration Phase 2 채널 확장 설계서 (L1) — Brevo+SNS 실연동 + 텔레그램 알림 date: 2026-02-26T00:30:00+09:00 type: design layer: L1 status: draft tags: [content-orchestration, phase2, brevo, sns, getlate, telegram, L1] author: self-healing-impl-pl project: content-orchestration reviewed_by: jarvis reviewed_at: "2026-02-25T18:25:00+09:00" approved_by: "" approved_at: ""

content-orchestration Phase 2 채널 확장 설계서 (L1)

작성일: 2026-02-26 작성자: self-healing-impl-pl 근거 문서:

  • L0: content-orchestration-phase2-backlog.md (Phase 2 백로그)
  • L1: content-orchestration-design-external.md (외부 연동 설계서, VP 승인)
  • L1: content-orchestration-design-pipeline.md (파이프라인 설계서, VP 승인)
  • 스킬: brevo-email-policy.md (CEO 확정 3단계 발송 정책)
  • 기존 코드: lib/brevo.ts, lib/getlate.ts, lib/sns-mock.ts, lib/publish-orchestrator.ts, lib/channels.ts, pipeline/publish-sns.ts

1. 배경 및 목적

1-1. Phase 1 현황

Phase 1에서 구현된 다채널 배포 시스템의 현재 상태:

채널Phase 1 상태상세
블로그 (apppro.kr)실연동 완료publish-orchestrator.ts → Turso blog_posts INSERT, published=1 즉시 발행
Brevo 뉴스레터실연동 완료sendCampaignScheduled() 즉시/예약 발송. 단, 오케스트레이터에서는 즉시 발송(null)만 사용
SNS (getlate.dev)mock 모드sns-mock.ts → 항상 success=false, mock=true 반환. channels에서 is_active=0
텔레그램 알림미구현파이프라인 이벤트 알림 없음. 에러 발생 시 대시보드에서만 확인 가능

1-2. Phase 2 목표

#목표백로그 ID기대 효과
1SNS mock → getlate.dev 실연동 교체EXT-5, BE-3콘텐츠 자동 SNS 배포 활성화, 도달 범위 확대
2Brevo 뉴스레터 자동 발송 고도화BE-2, EXT-2예약 발송 + 캠페인 상태 폴링 + 3단계 발송 정책 적용
3텔레그램 알림 통합EXT-1, SH-6, BE-4CEO/VP에게 주요 이벤트 자동 알림, 대시보드 확인 불필요

1-3. 범위 제한

  • 포함: SNS 실연동, Brevo 예약발송/상태폴링, 텔레그램 4종 알림
  • 제외: 블로그 발행 확인 폴링(EXT-4), Brevo/getlate 상태 폴링 Cron(별도 설계), 외부 플랫폼 확장(Phase 3)

2. Brevo 뉴스레터 자동 발송 설계

2-1. 현재 코드 분석

lib/brevo.ts (334줄):

  • sendCampaign(listId, subject, htmlContent): 즉시 발송만 지원
  • sendCampaignScheduled(listId, subject, htmlContent, scheduledAt?): 예약 발송 지원 (Phase 1 추가)
  • getCampaignStatus(campaignId): 캠페인 상태 조회 (Phase 1 추가, 수동 확인용)
  • mock 모드: BREVO_API_KEY 미설정 시 자동 진입

lib/publish-orchestrator.ts publishToBrevoChannel():

  • 현재: sendCampaignScheduled(listId, title, htmlContent, null)항상 즉시 발송
  • 한계: 예약 발송 미활용, 3단계 발송 정책 미적용, 캠페인 상태 추적 없음, 뉴스레터 HTML 템플릿 없음

2-2. Phase 2 변경 설계

2-2-1. 예약 발송 활성화

현재 publishToBrevoChannel()scheduledAt=null을 고정 전달하여 항상 즉시 발송된다. Phase 2에서는 channels.config에 발송 스케줄을 설정하여 예약 발송을 지원한다.

channels.config 확장:

{
  "list_id": 8,
  "template": "weekly",
  "sender_name": "AI AppPro",
  "sender_email": "hello@apppro.kr",
  "schedule": {
    "type": "delay",
    "delay_hours": 2
  }
}
  • schedule.type = "delay": approved 후 N시간 뒤 예약 발송
  • schedule.type = "fixed": 매주 특정 요일/시간 발송 (예: "day": "tuesday", "time": "10:00")
  • schedule.type = "immediate": 즉시 발송 (현재 동작, 기본값)

오케스트레이터 변경:

publishToBrevoChannel() 변경:
  1. channels.config에서 schedule 읽기
  2. schedule.type에 따라 scheduledAt 계산:
     - "immediate": null (즉시)
     - "delay": now + delay_hours (ISO 8601)
     - "fixed": 다음 매칭 시각 계산
  3. sendCampaignScheduled(listId, subject, html, scheduledAt) 호출
  4. content_distributions.scheduled_at에 예약 시각 기록

2-2-2. 3단계 발송 정책 적용 (CEO 확정 brevo-email-policy.md)

단계대상리스트승인구현 방식
1단계테스트 (3~5명)BREVO_LIST_ID_TEST자율channels.config.list_id = test list ID
2단계파일럿 (10~20명)BREVO_LIST_ID_PILOT자율channels.config에 pilot_list_id 추가
3단계전체 구독자BREVO_LIST_IDCEO 승인 필수channels.config.list_id = main list

구현 방식:

channels 테이블에 발송 단계별 별도 채널 레코드 생성이 아니라, channels.config.send_tier 필드로 관리한다:

{
  "list_id": 8,
  "send_tier": "test",
  "tier_config": {
    "test": { "list_id": 8, "auto": true },
    "pilot": { "list_id": 0, "auto": true },
    "production": { "list_id": 0, "auto": false, "requires_ceo_approval": true }
  }
}
  • send_tier: 현재 발송 단계 ("test" / "pilot" / "production")
  • 단계 승격은 대시보드 UI 또는 DB 수동 변경으로만 가능
  • production 단계는 requires_ceo_approval: true — 오케스트레이터가 이를 체크하여 CEO 승인 없이 전체 발송 차단

2-2-3. 캠페인 상태 폴링

기존 getCampaignStatus() 함수를 활용하여, 발송 후 결과를 자동 추적한다.

폴링 대상:

content_distributions에서:
  channel_id = 'ch-brevo'
  AND platform_status = 'registered' (예약 발송 상태)
  AND scheduled_at < now() (예약 시각 경과)

폴링 로직:

1. 위 조건의 distributions 조회
2. 각 distribution의 platform_id (= Brevo campaign_id)로 getCampaignStatus() 호출
3. 결과에 따라:
   - status='sent': platform_status → 'published', published_at 기록
   - status='queued': 대기 (다음 폴링에서 재확인)
   - status='draft': 아직 미발송 (Brevo 내부 지연)
   - 기타: error_logs 기록
4. metrics 업데이트: delivered, opens, clicks, bounces → content_distributions.metrics JSONB

폴링 시점:

  • 파이프라인 Cron(/api/cron/pipeline) 실행 시 함께 체크
  • 또는 별도 Cron 엔드포인트 /api/cron/check-distributions (Phase 2 후반)

2-2-4. 뉴스레터 HTML 템플릿 개선

현재 publishToBrevoChannel()이 content_body를 간단한 <br/> 치환으로만 HTML 변환한다.

Phase 2에서는 기존 newsletter-template.html 활용:

1. content_body JSON 파싱 → content, excerpt, category, tags 추출
2. prompts/newsletter-template.html 템플릿 로드
3. 플레이스홀더 치환: {{TITLE}}, {{CONTENT}}, {{EXCERPT}}, {{BLOG_URL}}
4. 완성된 HTML을 Brevo에 전달

3. SNS 자동 배포 설계 (getlate.dev 실연동)

3-1. 현재 코드 분석

lib/sns-mock.ts (37줄):

  • publishToSnsMock(): 항상 success=false, mock=true 반환
  • Phase 1에서 SNS 채널은 is_active=0이므로 호출되지 않음

lib/getlate.ts (258줄):

  • listAccounts(): 연결된 SNS 계정 조회
  • createPost(params): 멀티플랫폼 포스트 생성 (즉시/예약)
  • publishToSns(params): 통합 배포 (계정 조회 → 타겟 구성 → 포스트 생성)
  • mock 모드: GETLATE_API_KEY 미설정 시 자동 진입

pipeline/publish-sns.ts (303줄):

  • convertToSnsContent(): 플랫폼별 콘텐츠 변환 (twitter 280자, linkedin 3000자 등)
  • publishToSns(): CLI 용도의 getlate 발행 함수
  • getConnectedAccounts(): 계정 조회

lib/publish-orchestrator.ts publishToSnsChannel():

  • 현재: publishToSnsMock() 호출 → 항상 실패 반환
  • Phase 2에서 실연동으로 교체 필요

3-2. Phase 2 변경 설계

3-2-1. sns-mock.ts → getlate 실연동 교체

publish-orchestrator.tspublishToSnsChannel()을 수정하여, mock이 아닌 실제 getlate.dev API를 호출한다.

교체 전략:

sns-mock.ts를 삭제하지 않고, publishToSnsChannel() 내부에서 getChannelCredential() 결과에 따라 분기:

publishToSnsChannel(channel, contentId, title, contentBody):
  1. credential = getChannelCredential(channel)
  2. if (!credential):
     → 기존 mock 처리 (content_distributions.platform_status='failed', MOCK_MODE)
  3. if (credential):
     → content_body JSON 파싱 → slug, excerpt, tags 추출
     → convertToSnsContent() 호출 → 플랫폼별 콘텐츠 생성
     → getlate listAccounts() → 해당 플랫폼 계정 확인
     → getlate createPost() 호출 (즉시 또는 예약)
     → 결과에 따라 content_distributions 기록

3-2-2. 플랫폼별 채널 관리

Phase 1 시드 데이터에 이미 정의된 채널:

channel_idplatformis_active (Phase 1)is_active (Phase 2)
ch-twittertwitter01 (getlate 계정 연결 후)
ch-linkedinlinkedin01 (getlate 계정 연결 후)

Phase 2에서 추가 가능한 채널:

channel_idplatform조건
ch-threadsthreadsgetlate에서 Threads 계정 연결 시
ch-instagraminstagramgetlate에서 Instagram 계정 연결 시

채널 활성화 절차:

1. getlate.dev 대시보드에서 SNS 계정 연결
2. channels 테이블에서 해당 채널의 is_active = 1로 변경
3. 다음 파이프라인 실행 시 자동으로 해당 플랫폼에 배포

코드 변경 없이 DB 조작만으로 채널 추가/비활성화 가능 (channels 기반 동적 관리 원칙).

3-2-3. 콘텐츠 변환 로직 통합

현재 publish-sns.tsconvertToSnsContent()가 플랫폼별 콘텐츠 변환을 담당한다. 이 함수를 오케스트레이터에서 재사용한다.

오케스트레이터 → publish-sns.ts의 convertToSnsContent() import
  → title, excerpt, slug, tags, [channel.platform] 전달
  → 플랫폼 최적화된 콘텐츠 텍스트 반환

플랫폼별 변환 규칙 (기존 코드 유지):

플랫폼글자 제한포맷
Twitter/X280자{제목}\n\n{블로그URL}\n\n{해시태그}
Bluesky300자Twitter와 동일
LinkedIn3,000자{제목}\n\n{요약 전문}\n\n{블로그URL}\n\n{해시태그}
Threads500자{제목}\n\n{요약}\n\n{블로그URL}\n\n{해시태그}
Instagram2,200자Threads와 동일

3-2-4. SNS 배포 결과 추적

getlate POST /posts 응답에서 postId를 받아 content_distributions에 기록:

createPost() 응답:
  - postId: "post_xyz789"
  - status: "scheduled" / "published"

→ content_distributions:
  - platform_status: 'registered' (예약) 또는 'published' (즉시)
  - platform_id: "post_xyz789"
  - scheduled_at: 예약 시각 (예약 발행 시)

3-2-5. 부분 실패 처리

getlate는 단일 API 호출로 여러 플랫폼에 동시 발행한다. 일부 플랫폼만 실패하는 경우:

오케스트레이터에서 SNS 채널은 플랫폼별로 분리되어 있으므로
(ch-twitter, ch-linkedin 각각 별도 채널),
채널별로 개별 getlate 요청을 보낸다.

즉, ch-twitter 실패 → ch-twitter만 failed
    ch-linkedin 성공 → ch-linkedin은 registered/published

대안: 효율성을 위해 하나의 getlate 요청으로 여러 플랫폼을 동시 발행하는 옵션도 고려. 이 경우 channels 테이블에 type='sns'인 활성 채널을 모아 하나의 createPost(platforms: [...]) 호출 후, 응답에서 플랫폼별 결과를 분리하여 각 채널의 content_distributions에 기록.

권장: 효율성보다 오류 격리가 중요. 채널별 개별 요청 방식 채택. getlate Free 플랜 한도(30 포스트/월)를 고려하면 플랫폼 34개 × 월 8건 콘텐츠 = 2432건으로 한도에 근접. 만약 한도 초과 우려 시 배치 요청으로 전환.


4. 텔레그램 알림 설계

4-1. 설계 원칙

  1. Telegram API 직접 호출 금지 (CLAUDE.md 보안 정책)
  2. 기존 스크립트 (vice-reply.sh, ceo-reply.sh) 통해 간접 호출만 허용
  3. 파이프라인 코드에서 child_process.exec() 또는 child_process.execFile()로 스크립트 실행
  4. Vercel 서버리스 환경에서는 shell 실행 불가 → 대안 필요

4-2. Vercel 서버리스 환경 제약

content-pipeline은 Vercel에서 실행된다. Vercel 서버리스 함수에서는:

  • child_process.exec() 사용 불가 (shell 접근 제한)
  • 로컬 스크립트 파일(vice-reply.sh) 접근 불가

대안 방식: 텔레그램 알림 전용 HTTP 엔드포인트

자비스 스크립트가 Turso DB에 알림 레코드를 INSERT하고 텔레그램에 발송하는 대신, 파이프라인이 알림 데이터를 DB에 저장하고, 별도 스크립트(macOS 로컬)가 폴링하여 텔레그램으로 전송하는 방식을 채택한다.

4-3. 알림 아키텍처

[Vercel 파이프라인]                    [macOS 로컬]
       │                                    │
       │ pipeline_notifications              │
       │ 테이블에 INSERT                      │
       │ (type, title, body, status='pending')│
       │                                    │
       ↓                                    │
  content-os DB (Turso)  ←─── 폴링 ────→ notification-sender.sh
       │                                    │
       │                                    │ vice-reply.sh / ceo-reply.sh
       │                                    │
       │                                    ↓
       │                              Telegram Bot
       │                                    │
       └── status='sent' ←── UPDATE ────────┘

4-4. 알림 유형 정의

#이벤트알림 대상스크립트메시지 형식
1콘텐츠 생성 완료 (draft)VPvice-reply.sh콘텐츠 생성 완료\n제목: {title}\nQA: {score}/8\n미리보기: {url}
2QA 실패 (재시도 후 최종 실패)VPvice-reply.shQA 최종 실패\n제목: {topic}\n점수: {score}/8\n시도: {attempts}회
3배포 완료VPvice-reply.sh콘텐츠 배포 완료\n제목: {title}\n채널: {channels}\n블로그: {url}
4에러 에스컬레이션VPvice-reply.sh파이프라인 에러\n컴포넌트: {component}\n에러: {message}\n에러ID: {id}
5CEO 검수 요청CEOceo-reply.sh콘텐츠 검수 요청\n제목: {title}\n미리보기: {url}\n승인/거부 부탁드립니다

4-5. pipeline_notifications 테이블 설계

기존 DB 스키마에 새 테이블을 추가한다:

CREATE TABLE IF NOT EXISTS pipeline_notifications (
  id TEXT PRIMARY KEY,
  type TEXT NOT NULL,           -- 'draft_created' | 'qa_failed' | 'published' | 'error_escalation' | 'review_request'
  target TEXT NOT NULL,         -- 'vp' | 'ceo'
  title TEXT NOT NULL,          -- 알림 제목
  body TEXT NOT NULL,           -- 알림 본문 (줄바꿈 포함)
  content_id TEXT,              -- 관련 content_queue.id (선택)
  pipeline_log_id TEXT,         -- 관련 pipeline_logs.id (선택)
  error_log_id TEXT,            -- 관련 error_logs.id (선택)
  status TEXT NOT NULL DEFAULT 'pending',  -- 'pending' | 'sent' | 'failed'
  sent_at INTEGER,              -- 발송 완료 시각 (epoch ms)
  error_message TEXT,           -- 발송 실패 시 에러
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);

4-6. 알림 생성 함수

파이프라인 코드에서 호출하는 헬퍼 함수:

// lib/notifications.ts

export async function notifyDraftCreated(contentId: string, title: string, qaScore: number, previewUrl: string): Promise<void>
export async function notifyQaFailed(topic: string, score: number, attempts: number): Promise<void>
export async function notifyPublished(contentId: string, title: string, channels: string[], blogUrl: string): Promise<void>
export async function notifyErrorEscalation(component: string, errorMessage: string, errorLogId: string): Promise<void>
export async function notifyReviewRequest(contentId: string, title: string, previewUrl: string): Promise<void>

각 함수는:

  1. 메시지 포맷팅
  2. pipeline_notifications 테이블에 INSERT (status='pending')
  3. 반환 (Vercel에서의 역할은 여기까지)

4-7. 알림 발송기 (macOS 로컬 스크립트)

#!/bin/bash
# scripts/notification-sender.sh
# macOS LaunchAgent 또는 crontab으로 5분마다 실행

# 1. Turso에서 pending 알림 조회
PENDING=$(turso db shell content-os "SELECT id, type, target, title, body FROM pipeline_notifications WHERE status='pending' ORDER BY created_at ASC LIMIT 10")

# 2. 각 알림에 대해 스크립트 호출
for notification in $PENDING; do
  if [ "$target" = "vp" ]; then
    vice-reply.sh "$body" "pipeline-alert"
  elif [ "$target" = "ceo" ]; then
    ceo-reply.sh "$body" "vice-claude"
  fi

  # 3. 발송 성공 시 status='sent' 업데이트
  turso db shell content-os "UPDATE pipeline_notifications SET status='sent', sent_at=$(date +%s)000 WHERE id='$id'"
done

실제 구현 시 JSON 파싱 등 정교한 처리 필요. 위는 개념 설명용 의사 코드.

4-8. 알림 호출 위치 (파이프라인 코드)

파일위치알림 함수
stage-generate.ts생성 성공 후 (line ~168)notifyDraftCreated()
stage-generate.ts최종 실패 시 (line ~163)notifyQaFailed()
stage-publish.ts배포 성공 후 (line ~136)notifyPublished()
lib/self-healing.tsescalateL5() 호출 후notifyErrorEscalation()
stage-publish.ts (향후)approved → reviewing 전환 시notifyReviewRequest()

5. DB 스키마 변경

5-1. 신규 테이블

pipeline_notifications

CREATE TABLE IF NOT EXISTS pipeline_notifications (
  id TEXT PRIMARY KEY,
  type TEXT NOT NULL,
  target TEXT NOT NULL,
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  content_id TEXT,
  pipeline_log_id TEXT,
  error_log_id TEXT,
  status TEXT NOT NULL DEFAULT 'pending',
  sent_at INTEGER,
  error_message TEXT,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_notifications_status ON pipeline_notifications(status);
CREATE INDEX IF NOT EXISTS idx_notifications_type ON pipeline_notifications(type);

5-2. 기존 테이블 변경

channels.config 확장 (스키마 변경 없음, JSON 필드)

Brevo 채널 config에 필드 추가:

// ch-brevo 기존
{
  "list_id": 8,
  "template": "weekly",
  "sender_name": "AI AppPro",
  "sender_email": "hello@apppro.kr"
}

// ch-brevo Phase 2
{
  "list_id": 8,
  "template": "weekly",
  "sender_name": "AI AppPro",
  "sender_email": "hello@apppro.kr",
  "send_tier": "test",
  "tier_config": {
    "test": { "list_id": 8, "auto": true },
    "pilot": { "list_id": 0, "auto": true },
    "production": { "list_id": 0, "auto": false, "requires_ceo_approval": true }
  },
  "schedule": {
    "type": "immediate"
  }
}

content_distributions.metrics 컬럼 추가

ALTER TABLE content_distributions ADD COLUMN metrics TEXT;
-- JSON: {"delivered": N, "opens": N, "clicks": N, "bounces": N}
-- Brevo 캠페인 상태 폴링 시 업데이트

channels 테이블 SNS 채널 is_active 변경

-- getlate 계정 연결 확인 후
UPDATE channels SET is_active = 1, updated_at = {now} WHERE id = 'ch-twitter';
UPDATE channels SET is_active = 1, updated_at = {now} WHERE id = 'ch-linkedin';

6. API 변경

6-1. 기존 API 수정

/api/cron/pipeline (route.ts)

파이프라인 Cron에 알림 생성 로직 추가:

기존 플로우:
  1. ensureSchema()
  2. runCollectStage()
  3. runGenerateStage()

Phase 2 추가:
  4. 생성 결과에 따라 알림 생성:
     - 성공: notifyDraftCreated()
     - 실패: notifyQaFailed()
  5. (선택) Brevo/SNS 배포 상태 폴링

/api/pipeline/content/[id]/approve (기존 승인 API)

승인 시 다채널 배포 트리거에 SNS 실연동 포함:

기존: publishToAllChannels() → 블로그 + Brevo + SNS(mock)
Phase 2: publishToAllChannels() → 블로그 + Brevo + SNS(getlate 실연동)
  + notifyPublished() 알림 생성

6-2. 신규 API

GET /api/pipeline/notifications (알림 조회)

대시보드에서 최근 알림 목록을 표시하기 위한 API.

Query params: ?status=pending|sent|failed&limit=20
Response: { notifications: [...], total: N }

POST /api/cron/check-distributions (선택, Phase 2 후반)

Brevo 캠페인 상태 + getlate 포스트 상태를 폴링하는 전용 Cron.

1. content_distributions에서 platform_status='registered' 조회
2. 채널 type별:
   - newsletter: getCampaignStatus() 호출
   - sns: getlate GET /posts/{id} 호출 (Phase 2 후반)
3. 결과에 따라 platform_status 업데이트

7. 구현 작업 목록

Phase 2-A: 핵심 기능 (1주)

#작업파일난이도의존성
1SNS 실연동: publishToSnsChannel() 수정 — mock → getlate 실호출publish-orchestrator.tsgetlate.ts 기존 코드 활용
2플랫폼별 콘텐츠 변환 통합: convertToSnsContent() import + 오케스트레이터 연동publish-orchestrator.ts, publish-sns.tsTask 1
3channels is_active 업데이트: ch-twitter, ch-linkedin 활성화DB 마이그레이션getlate 계정 연결 확인
4pipeline_notifications 테이블 생성: ensureSchema() 확장content-db.ts
5알림 생성 함수: lib/notifications.ts 신규lib/notifications.tsTask 4
6파이프라인 알림 연동: stage-generate, stage-publish에 알림 호출 추가stage-generate.ts, stage-publish.tsTask 5

Phase 2-B: 고도화 (2주째)

#작업파일난이도의존성
7Brevo 예약 발송: channels.config.schedule 기반 예약 발송publish-orchestrator.ts
8Brevo 3단계 발송 정책: send_tier + tier_config 적용publish-orchestrator.tsTask 7
9Brevo 캠페인 상태 폴링: getCampaignStatus() 자동 호출publish-orchestrator.ts 또는 별도 모듈
10content_distributions.metrics 컬럼 추가: ALTER TABLEcontent-db.tsTask 9
11알림 발송기 스크립트: macOS 로컬 notification-sender.shscripts/notification-sender.shTask 4
12알림 조회 API: GET /api/pipeline/notificationssrc/app/api/pipeline/notifications/Task 4
13뉴스레터 HTML 템플릿 적용: newsletter-template.html 연동publish-orchestrator.ts

8. 위험 요소

리스크확률영향대응
getlate Free 플랜 한도 초과 (30 포스트/월)SNS 배포 중단채널별 개별 요청 시 월 30건 소진 빠름. 배치 요청 전환 또는 유료 플랜 검토. 초기에는 Twitter+LinkedIn 2개만 활성화 → 월 16건
getlate API 키 미설정SNS 배포 불가mock 모드 유지 (파이프라인 중단 없음). CEO에게 API 키 확인 요청
Brevo 일일 발송 한도 (300통/일 Free)뉴스레터 발송 지연구독자 300명 이하 시 문제 없음. 초과 시 예약 발송으로 다음 날 분배
Vercel 서버리스에서 shell 실행 불가확정텔레그램 알림 직접 발송 불가DB 기반 알림 큐 + macOS 로컬 폴링 방식 채택 (Section 4)
텔레그램 알림 폴링 지연 (최대 5분)긴급 에러 알림 지연폴링 주기 단축 (1분) 또는 self-healing L5 에스컬레이션 시 별도 즉시 처리
CEO 승인 없이 전체 발송높(위반 시)정책 위반, 스팸 리스크send_tier='production'일 때 requires_ceo_approval 체크 강제. 오케스트레이터에서 차단
channels.config JSON 파싱 오류채널 설정 못 읽음parseChannelConfig() 기존 try-catch 처리. 파싱 실패 시 기본값 사용
SNS 콘텐츠 글자 수 초과발행 실패convertToSnsContent() 기존 글자 수 체크 + 해시태그 제거 로직 활용

리뷰 로그

[self-healing-impl-pl 초안 작성] 2026-02-26 00:30

  • Phase 2 백로그 + L1 외부 연동 설계서 + 파이프라인 설계서 기반으로 작성
  • 기존 코드 6개 파일 분석: brevo.ts, getlate.ts, sns-mock.ts, publish-orchestrator.ts, channels.ts, publish-sns.ts
  • Brevo 설계: 예약 발송(channels.config.schedule), 3단계 발송 정책(send_tier), 캠페인 상태 폴링(getCampaignStatus), HTML 템플릿
  • SNS 설계: sns-mock → getlate 실연동 교체, 채널별 개별 요청, convertToSnsContent() 재사용, 부분 실패 처리
  • 텔레그램 설계: Vercel 서버리스 shell 제약 → DB 기반 알림 큐(pipeline_notifications) + macOS 로컬 폴링 방식
  • DB 변경: pipeline_notifications 신규 테이블, content_distributions.metrics 컬럼, channels.config JSON 확장
  • API 변경: Cron 알림 추가, 알림 조회 API, 배포 상태 폴링 Cron
  • 구현 작업 13건: Phase 2-A(핵심, 1주) 6건 + Phase 2-B(고도화, 2주째) 7건
  • 위험 요소 8건 식별 + 대응 방안
  • brevo-email-policy.md 스킬 3단계 정책 반영 (send_tier + requires_ceo_approval)
  • 자비스 검수 요청

[자비스 검수] 2026-02-25T18:25:00+09:00

  • ✅ Phase 1 현황 정확 분석 (블로그/Brevo 실연동, SNS mock, 텔레그램 미구현 상태)
  • ✅ Brevo 예약 발송: channels.config.schedule 3가지 모드 (immediate/delay/fixed) 설계 명확
  • ✅ 3단계 발송 정책: send_tier + tier_config + requires_ceo_approval — brevo-email-policy.md 완전 반영
  • ✅ SNS 실연동: mock → getlate 분기 전략, convertToSnsContent() 재사용 (코드 중복 없음)
  • ✅ 텔레그램 Vercel 서버리스 shell 제약 완벽 파악 → DB 기반 알림 큐 + macOS 로컬 폴링 아키텍처
  • ✅ Telegram API 직접 호출 금지 정책 준수 (기존 스크립트 간접 호출 방식)
  • ✅ pipeline_notifications 신규 테이블 설계 + 인덱스 포함
  • ✅ 구현 13건 / Phase 2-A(핵심, 1주) + Phase 2-B(고도화, 2주) 분리 적절
  • ✅ 위험 요소 8건 + 대응 방안 (getlate Free 30건/월 한도 포함)
  • ⚠️ notification-sender.sh 스크립트 — 의사코드 수준으로 JSON 파싱 정교화 필요 (L2 구현 시 처리)
  • ⚠️ getlate API 키 미설정 현황 — CEO 블로킹 (SNS 채널 활성화 전 확인 필요)
  • 검수 판정: ✅ VP 승인 요청 가능
plans/2026/02/25/content-orchestration-design-phase2-channels.md