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 | 기대 효과 |
|---|---|---|---|
| 1 | SNS mock → getlate.dev 실연동 교체 | EXT-5, BE-3 | 콘텐츠 자동 SNS 배포 활성화, 도달 범위 확대 |
| 2 | Brevo 뉴스레터 자동 발송 고도화 | BE-2, EXT-2 | 예약 발송 + 캠페인 상태 폴링 + 3단계 발송 정책 적용 |
| 3 | 텔레그램 알림 통합 | EXT-1, SH-6, BE-4 | CEO/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_ID | CEO 승인 필수 | 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.ts의 publishToSnsChannel()을 수정하여, 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_id | platform | is_active (Phase 1) | is_active (Phase 2) |
|---|---|---|---|
ch-twitter | 0 | 1 (getlate 계정 연결 후) | |
ch-linkedin | 0 | 1 (getlate 계정 연결 후) |
Phase 2에서 추가 가능한 채널:
| channel_id | platform | 조건 |
|---|---|---|
ch-threads | threads | getlate에서 Threads 계정 연결 시 |
ch-instagram | getlate에서 Instagram 계정 연결 시 |
채널 활성화 절차:
1. getlate.dev 대시보드에서 SNS 계정 연결
2. channels 테이블에서 해당 채널의 is_active = 1로 변경
3. 다음 파이프라인 실행 시 자동으로 해당 플랫폼에 배포
코드 변경 없이 DB 조작만으로 채널 추가/비활성화 가능 (channels 기반 동적 관리 원칙).
3-2-3. 콘텐츠 변환 로직 통합
현재 publish-sns.ts의 convertToSnsContent()가 플랫폼별 콘텐츠 변환을 담당한다. 이 함수를 오케스트레이터에서 재사용한다.
오케스트레이터 → publish-sns.ts의 convertToSnsContent() import
→ title, excerpt, slug, tags, [channel.platform] 전달
→ 플랫폼 최적화된 콘텐츠 텍스트 반환
플랫폼별 변환 규칙 (기존 코드 유지):
| 플랫폼 | 글자 제한 | 포맷 |
|---|---|---|
| Twitter/X | 280자 | {제목}\n\n{블로그URL}\n\n{해시태그} |
| Bluesky | 300자 | Twitter와 동일 |
| 3,000자 | {제목}\n\n{요약 전문}\n\n{블로그URL}\n\n{해시태그} | |
| Threads | 500자 | {제목}\n\n{요약}\n\n{블로그URL}\n\n{해시태그} |
| 2,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. 설계 원칙
- Telegram API 직접 호출 금지 (CLAUDE.md 보안 정책)
- 기존 스크립트 (
vice-reply.sh,ceo-reply.sh) 통해 간접 호출만 허용 - 파이프라인 코드에서
child_process.exec()또는child_process.execFile()로 스크립트 실행 - 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) | VP | vice-reply.sh | 콘텐츠 생성 완료\n제목: {title}\nQA: {score}/8\n미리보기: {url} |
| 2 | QA 실패 (재시도 후 최종 실패) | VP | vice-reply.sh | QA 최종 실패\n제목: {topic}\n점수: {score}/8\n시도: {attempts}회 |
| 3 | 배포 완료 | VP | vice-reply.sh | 콘텐츠 배포 완료\n제목: {title}\n채널: {channels}\n블로그: {url} |
| 4 | 에러 에스컬레이션 | VP | vice-reply.sh | 파이프라인 에러\n컴포넌트: {component}\n에러: {message}\n에러ID: {id} |
| 5 | CEO 검수 요청 | CEO | ceo-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>
각 함수는:
- 메시지 포맷팅
- pipeline_notifications 테이블에 INSERT (status='pending')
- 반환 (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.ts | escalateL5() 호출 후 | 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주)
| # | 작업 | 파일 | 난이도 | 의존성 |
|---|---|---|---|---|
| 1 | SNS 실연동: publishToSnsChannel() 수정 — mock → getlate 실호출 | publish-orchestrator.ts | 중 | getlate.ts 기존 코드 활용 |
| 2 | 플랫폼별 콘텐츠 변환 통합: convertToSnsContent() import + 오케스트레이터 연동 | publish-orchestrator.ts, publish-sns.ts | 낮 | Task 1 |
| 3 | channels is_active 업데이트: ch-twitter, ch-linkedin 활성화 | DB 마이그레이션 | 낮 | getlate 계정 연결 확인 |
| 4 | pipeline_notifications 테이블 생성: ensureSchema() 확장 | content-db.ts | 낮 | — |
| 5 | 알림 생성 함수: lib/notifications.ts 신규 | lib/notifications.ts | 중 | Task 4 |
| 6 | 파이프라인 알림 연동: stage-generate, stage-publish에 알림 호출 추가 | stage-generate.ts, stage-publish.ts | 중 | Task 5 |
Phase 2-B: 고도화 (2주째)
| # | 작업 | 파일 | 난이도 | 의존성 |
|---|---|---|---|---|
| 7 | Brevo 예약 발송: channels.config.schedule 기반 예약 발송 | publish-orchestrator.ts | 중 | — |
| 8 | Brevo 3단계 발송 정책: send_tier + tier_config 적용 | publish-orchestrator.ts | 중 | Task 7 |
| 9 | Brevo 캠페인 상태 폴링: getCampaignStatus() 자동 호출 | publish-orchestrator.ts 또는 별도 모듈 | 중 | — |
| 10 | content_distributions.metrics 컬럼 추가: ALTER TABLE | content-db.ts | 낮 | Task 9 |
| 11 | 알림 발송기 스크립트: macOS 로컬 notification-sender.sh | scripts/notification-sender.sh | 중 | Task 4 |
| 12 | 알림 조회 API: GET /api/pipeline/notifications | src/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 승인 요청 가능