콘텐츠 오케스트레이션 완전체 — 시스템 설계 기획서
버전: v1.0 작성일: 2026-03-01 작성자: Product Architect PL 검토 대상: VP (머스크), CEO (김준영 대표) 프로젝트: content-orchestration (https://content-orchestration.vercel.app)
목차
1. 시스템 아키텍처 개요
1-1. 현재 상태 (AS-IS)
현재 content-orchestration은 읽기 전용 대시보드 수준이다.
| 기능 | 상태 | 설명 |
|---|---|---|
| 콘텐츠 목록 조회 | 완료 | content_queue 테이블 기반, 필터 (status/channel) |
| 승인/반려 버튼 | 완료 | Server Action으로 status 업데이트 |
| 캘린더 뷰 | 완료 | scheduled_at 기반 월별 표시 |
| RSS 수집 모니터 | 완료 | collected_news 통계 표시 |
| 파이프라인 로그 | 완료 | pipeline_logs 조회 |
| 성과 분석 | 완료 | 차트 기반 analytics 대시보드 |
| 콘텐츠 생성/편집 | 미구현 | 콘텐츠 body 편집 불가 |
| 예약 발행 설정 | 미구현 | UI에서 scheduled_at 설정 불가 |
| 플랫폼 API 연동 | 미구현 | 자동 배포 기능 없음 |
| 배포 결과 추적 | 미구현 | publish_logs 없음 |
1-2. 목표 상태 (TO-BE)
CEO가 대시보드 하나로 콘텐츠 생성 → 편집 → 승인 → 예약 → 자동 배포 → 결과 확인까지 완수하는 통합 플랫폼.
1-3. 컴포넌트 다이어그램
+------------------------------------------------------------------+
| content-orchestration (Vercel) |
| |
| +------------------+ +------------------+ +------------------+ |
| | 콘텐츠 에디터 | | 워크플로우 관리 | | 스케줄러 UI | |
| | (Markdown) | | (상태 머신) | | (Datetime) | |
| +--------+---------+ +--------+---------+ +--------+---------+ |
| | | | |
| v v v |
| +----------------------------------------------------------+ |
| | Server Actions | |
| | createContent / updateContent / approveContent | |
| | scheduleContent / triggerPublish / retryPublish | |
| +-----+---------------------------+-------------------+-----+ |
| | | | |
| v v v |
| +------------+ +---------------+ +-----------+ |
| | content-os | | publish_logs | | platform | |
| | (Turso DB) | | (Turso DB) | | _configs | |
| +------------+ +---------------+ +-----------+ |
+------------------------------------------------------------------+
| |
v v
+------------------+ +-------------------+ +-----------------+
| apppro.kr | | Brevo API | | GetRates API |
| POST /api/blog | | POST /v3/smtp | | (SNS 배포) |
| /publish | | /emailCampaigns | | |
+------------------+ +-------------------+ +-----------------+
1-4. 데이터 흐름
[CEO/VP 브라우저]
|
| (1) 콘텐츠 작성/편집
v
[content-orchestration UI]
|
| (2) Server Action → Turso DB 저장
v
[content_queue 테이블]
|
| (3) status = 'approved' + scheduled_at 도달
v
[Cron / Manual Trigger]
|
| (4) 플랫폼별 API 호출
+-----> apppro.kr /api/blog/publish (블로그 게시)
+-----> Brevo /v3/smtp/email (이메일 발송)
+-----> GetRates API (SNS 배포)
|
| (5) 결과 → publish_logs 기록
v
[publish_logs 테이블]
|
| (6) 대시보드에서 결과 확인
v
[CEO/VP 브라우저]
2. DB 스키마 변경사항
2-1. content_queue 테이블 — 추가 컬럼
현재 content_queue 스키마에 다음 컬럼을 추가한다.
-- 플랫폼 대상 목록 (JSON 배열)
-- 예: ["blog.apppro.kr", "brevo", "getrates"]
ALTER TABLE content_queue ADD COLUMN platform_targets TEXT DEFAULT '[]';
-- 플랫폼별 배포 결과 (JSON 객체)
-- 예: {"blog.apppro.kr": {"status": "success", "url": "..."}, "brevo": {"status": "pending"}}
ALTER TABLE content_queue ADD COLUMN publish_results TEXT DEFAULT '{}';
-- 예약 배포 시각 (ISO 8601 문자열 또는 Unix ms)
-- 기존 scheduled_at 컬럼 활용 (이미 INTEGER로 존재)
-- 콘텐츠 메타데이터 (JSON)
-- 예: {"seo_title": "...", "description": "...", "tags": ["ai", "marketing"]}
ALTER TABLE content_queue ADD COLUMN metadata TEXT DEFAULT '{}';
2-2. 신규 테이블: platform_configs
플랫폼별 연동 설정을 저장한다. 환경변수 대신 DB에서 관리하여 런타임에 조회 가능하도록 한다.
CREATE TABLE IF NOT EXISTS platform_configs (
id TEXT PRIMARY KEY, -- 예: 'blog.apppro.kr'
name TEXT NOT NULL, -- 표시명: 'AppPro 블로그'
platform_type TEXT NOT NULL, -- 'blog' | 'email' | 'sns'
api_endpoint TEXT, -- API URL
auth_type TEXT DEFAULT 'api_key', -- 'api_key' | 'bearer' | 'none'
auth_key_env TEXT, -- 환경변수 이름 (실제 키는 env에 보관)
config_json TEXT DEFAULT '{}', -- 플랫폼별 추가 설정 (JSON)
is_active INTEGER DEFAULT 1, -- 활성/비활성
created_at INTEGER DEFAULT (unixepoch()*1000),
updated_at INTEGER DEFAULT (unixepoch()*1000)
);
초기 데이터:
INSERT INTO platform_configs (id, name, platform_type, api_endpoint, auth_key_env, config_json) VALUES
('blog.apppro.kr', 'AppPro 블로그', 'blog', 'https://apppro.kr/api/blog/publish', 'APPPRO_BLOG_API_KEY', '{}'),
('brevo', 'Brevo 이메일', 'email', 'https://api.brevo.com/v3/smtp/email', 'BREVO_API_KEY', '{"sender_email": "contact@apppro.kr", "sender_name": "AppPro AI"}'),
('getrates', 'GetRates SNS', 'sns', '', 'GETRATES_API_KEY', '{}');
2-3. 신규 테이블: publish_logs
배포 시도마다 1건씩 기록한다.
CREATE TABLE IF NOT EXISTS publish_logs (
id TEXT PRIMARY KEY, -- UUID
content_id TEXT NOT NULL, -- content_queue.id FK
platform_id TEXT NOT NULL, -- platform_configs.id FK
status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'success' | 'failed' | 'retrying'
request_payload TEXT, -- 전송한 요청 body (JSON, 디버깅용)
response_status INTEGER, -- HTTP 상태 코드
response_body TEXT, -- API 응답 body (요약)
error_message TEXT, -- 실패 시 에러 메시지
retry_count INTEGER DEFAULT 0, -- 재시도 횟수
published_url TEXT, -- 성공 시 배포된 URL
triggered_by TEXT DEFAULT 'scheduler', -- 'scheduler' | 'manual' | 'retry'
created_at INTEGER DEFAULT (unixepoch()*1000),
completed_at INTEGER -- 완료 시각
);
CREATE INDEX idx_publish_logs_content ON publish_logs(content_id);
CREATE INDEX idx_publish_logs_status ON publish_logs(status);
2-4. 전체 ERD (텍스트)
content_queue (기존 + 확장)
├── id (PK)
├── type, pillar, topic, title
├── content_body (마크다운 본문)
├── status (draft → review → approved → scheduled → publishing → published / failed)
├── priority, channel, project
├── platform_targets (JSON 배열) ★ NEW
├── publish_results (JSON 객체) ★ NEW
├── metadata (JSON) ★ NEW
├── approved_by, approved_at
├── rejected_reason
├── scheduled_at
├── created_at, updated_at
└──── 1:N → publish_logs
platform_configs ★ NEW
├── id (PK) — 'blog.apppro.kr', 'brevo', 'getrates'
├── name, platform_type
├── api_endpoint, auth_type, auth_key_env
├── config_json
└── is_active
publish_logs ★ NEW
├── id (PK)
├── content_id (FK → content_queue)
├── platform_id (FK → platform_configs)
├── status, error_message
├── request_payload, response_status, response_body
├── retry_count, published_url
├── triggered_by
└── created_at, completed_at
3. 기능 명세
F1. 콘텐츠 에디터
목적: 대시보드 내에서 콘텐츠를 직접 생성하고 편집한다.
F1-1. 콘텐츠 생성
| 항목 | 설명 |
|---|---|
| 진입점 | /[project]/content 페이지 상단 "새 콘텐츠" 버튼 |
| 경로 | /[project]/content/new |
| 입력 필드 | title (필수), type (blog/sns/newsletter), pillar, channel, content_body (Markdown textarea) |
| 저장 | Server Action → content_queue INSERT, status='draft' |
| 초기 platform_targets | 빈 배열 [] (이후 F3에서 설정) |
F1-2. 콘텐츠 편집
| 항목 | 설명 |
|---|---|
| 진입점 | 콘텐츠 목록에서 제목 클릭 |
| 경로 | /[project]/content/[id] |
| 편집 가능 상태 | draft, review (approved 이후는 읽기 전용) |
| 에디터 | Markdown textarea + 실시간 미리보기 (2-pane 레이아웃) |
| 미리보기 | react-markdown + remark-gfm 으로 렌더링 |
| 저장 | Server Action → content_queue UPDATE (title, content_body, metadata 등) |
F1-3. 에디터 UI 와이어프레임
+------------------------------------------------------------------+
| [← 목록으로] [저장] [미리보기] [삭제] |
+------------------------------------------------------------------+
| |
| 제목: [_____________________________________________________] |
| |
| 유형: [blog ▼] 필라: [ai-tools ▼] 채널: [blog.apppro.kr ▼] |
| |
| +----------------------------+ +------------------------------+ |
| | Markdown 편집기 | | 미리보기 | |
| | | | | |
| | # 제목 | | 제목 (h1 렌더링) | |
| | | | | |
| | 본문 내용을 입력... | | 본문 내용 (prose) | |
| | | | | |
| +----------------------------+ +------------------------------+ |
| |
| SEO 제목: [_____________________] |
| 설명: [_____________________________________________________] |
| 태그: [ai] [marketing] [+ 추가] |
| |
+------------------------------------------------------------------+
F1-4. 기술 구현
- 에디터 컴포넌트:
"use client"컴포넌트,<textarea>기반 (외부 의존성 최소화) - 미리보기:
react-markdown+remark-gfm(기존 다른 프로젝트에서 사용 중이므로 패턴 동일) - 저장 방식: Server Action (
createContent,updateContent) - 자동 저장: 입력 후 3초 디바운스로 draft 자동 저장 (옵션)
F2. Status 워크플로우
목적: 콘텐츠의 라이프사이클을 상태 머신으로 관리한다.
F2-1. 상태 전이 다이어그램
CEO 반려
+------ rejected <----+----+
| | |
v | |
(생성) → draft ─── 검수 요청 ─→ review ─── CEO 승인 ─→ approved
^ |
| 예약 설정 | 즉시 배포
| +──────────+──────+
| v v
| scheduled publishing
| | |
| 시각 도달| 성공 |실패
| v +────────+────+
| publishing | |
| | v v
| +────────+ published failed
| | |
+───── (draft로 복귀 편집) ─+── 재시도 ────────────────────+
F2-2. 상태 정의
| 상태 | 설명 | 허용 액션 |
|---|---|---|
draft | 작성 중 | 편집, 검수 요청, 승인, 반려 |
review | 검수 대기 | 승인, 반려 |
approved | 승인 완료 | 예약 설정, 즉시 배포 |
scheduled | 예약 대기 | 예약 취소 (→ approved), 즉시 배포 |
publishing | 배포 진행 중 | 없음 (시스템 전용) |
published | 배포 완료 | 없음 |
failed | 배포 실패 | 재시도, draft로 복귀 |
rejected | 반려됨 | draft로 복귀 (편집 후 재제출) |
F2-3. CEO 승인 액션
- 현재: 버튼 1개 (무조건
approved_by: 'VP/CEO') - 변경: 승인자 구분 가능 (VP 검수 vs CEO 최종 승인)
- Server Action
approveContent(id, approvedBy: string)에서approved_by파라미터 사용
F3. 플랫폼 연동 설정
목적: 콘텐츠별로 어떤 플랫폼에 배포할지 선택하고, 플랫폼별 예약 시각을 설정한다.
F3-1. 플랫폼 선택 UI
콘텐츠 상세 페이지(/[project]/content/[id])의 하단에 배치한다.
+------------------------------------------------------------------+
| 배포 플랫폼 설정 |
+------------------------------------------------------------------+
| |
| [x] AppPro 블로그 (blog.apppro.kr) |
| 예약 시각: [2026-03-05] [09:00] [KST ▼] |
| |
| [x] Brevo 이메일 |
| 예약 시각: [2026-03-05] [10:00] [KST ▼] |
| 발신자: contact@apppro.kr |
| 대상 리스트: [전체 구독자 ▼] |
| |
| [ ] GetRates SNS |
| (미연동 — API 키 필요) |
| |
| [설정 저장] |
+------------------------------------------------------------------+
F3-2. 데이터 모델
platform_targets JSON 배열로 저장:
[
{
"platform_id": "blog.apppro.kr",
"scheduled_at": 1741150800000,
"config": {}
},
{
"platform_id": "brevo",
"scheduled_at": 1741154400000,
"config": {
"list_id": 3,
"subject_override": "이번 주 AI 뉴스"
}
}
]
F3-3. 플랫폼별 추가 설정
| 플랫폼 | 추가 설정 항목 |
|---|---|
| blog.apppro.kr | slug (자동 생성 또는 수동), category, tags |
| brevo | subject (이메일 제목), list_id, sender |
| getrates | SNS 플랫폼 선택 (twitter/instagram/linkedin), 해시태그 |
F4. 자동 배포 트리거
목적: 승인된 콘텐츠를 예약 시각에 맞춰 자동으로 배포하거나, 수동으로 즉시 배포한다.
F4-1. 트리거 방식
| 방식 | 조건 | 구현 |
|---|---|---|
| 예약 자동 배포 | status='scheduled' AND scheduled_at <= now | Vercel Cron Job (매 5분) |
| 즉시 배포 | status='approved', CEO가 "즉시 배포" 클릭 | Server Action → 즉시 API 호출 |
| 재시도 | status='failed', "재시도" 클릭 | Server Action → API 재호출 |
F4-2. Vercel Cron Job 설계
vercel.json:
{
"crons": [
{
"path": "/api/cron/publish",
"schedule": "*/5 * * * *"
}
]
}
/api/cron/publish 로직:
content_queue에서status='scheduled' AND scheduled_at <= Date.now()조회- 각 콘텐츠의
platform_targets파싱 - 해당 시각이 도달한 플랫폼별로 배포 API 호출
publish_logs기록- 모든 플랫폼 완료 시
status='published', 하나라도 실패 시status='failed'
F4-3. 즉시 배포 플로우
CEO 클릭 "즉시 배포"
|
v
Server Action: publishNow(contentId)
|
| 1. status → 'publishing'
| 2. platform_targets 파싱
| 3. 각 플랫폼 API 순차 호출
| 4. publish_logs 기록
| 5. 전체 성공 → status = 'published'
| 부분 실패 → status = 'failed' + publish_results 업데이트
v
결과 표시 (revalidatePath)
F4-4. 재시도 메커니즘
- 최대 재시도 횟수: 3회
- 재시도 간격: 즉시 (수동 트리거)
- 재시도 대상: publish_logs에서 status='failed'인 항목만
- 재시도 시: retry_count 증가, 새로운 publish_logs 레코드 생성
F5. 플랫폼별 API 연동
F5-1. apppro.kr 블로그
현재 상태: apppro.kr에 블로그 게시 API가 없음. 새로 구축 필요.
apppro.kr 측 구현 필요 API:
POST /api/blog/publish
Request:
{
"title": "AI 도구로 업무 효율 200% 올리는 방법",
"slug": "ai-tools-productivity",
"content": "# 서론\n\n마크다운 본문...",
"category": "ai-tools",
"tags": ["ai", "생산성", "도구"],
"seo_title": "AI 도구 생산성 가이드 | AppPro",
"seo_description": "AI 도구를 활용하여...",
"published": true,
"author": "AppPro AI"
}
Response (성공):
{
"success": true,
"slug": "ai-tools-productivity",
"url": "https://apppro.kr/blog/ai-tools-productivity",
"published_at": "2026-03-05T09:00:00+09:00"
}
Response (실패):
{
"success": false,
"error": "Slug already exists",
"code": "DUPLICATE_SLUG"
}
인증: x-api-key 헤더 (환경변수 APPPRO_BLOG_API_KEY)
주의: apppro.kr은 현재 파일 기반 블로그 (content/blog/ 디렉토리의 .mdx 파일). API 게시를 구현하려면:
- Option A: API가 .mdx 파일을 생성하고 Git push → Vercel 재배포 (시간 소요)
- Option B: DB 기반 블로그로 전환 (대규모 리팩토링)
- 권장: Option A — 기존 아키텍처 유지하면서 API 엔드포인트만 추가
F5-2. Brevo 이메일
현재 상태: KoreaAI Hub에 Brevo 연동 구현 완료. 동일 패턴 재활용 가능.
사용할 Brevo API:
POST https://api.brevo.com/v3/smtp/email
Request:
{
"sender": {
"name": "AppPro AI",
"email": "contact@apppro.kr"
},
"to": [
{"email": "subscriber@example.com", "name": "구독자"}
],
"subject": "[AppPro] AI 도구로 업무 효율 200% 올리는 방법",
"htmlContent": "<html><body>...(마크다운→HTML 변환)...</body></html>",
"scheduledAt": "2026-03-05T10:00:00+09:00"
}
대량 발송 방식:
- Brevo Contact List 기반:
/v3/emailCampaigns사용 - 또는 KoreaAI Hub 방식처럼 subscribers DB에서 가져와
/v3/smtp/email반복 호출
캠페인 방식 (대량 발송용):
POST https://api.brevo.com/v3/emailCampaigns
{
"name": "AppPro Newsletter #1",
"subject": "[AppPro] AI 도구 가이드",
"sender": {
"name": "AppPro AI",
"email": "contact@apppro.kr"
},
"type": "classic",
"htmlContent": "<html>...</html>",
"recipients": {
"listIds": [3]
},
"scheduledAt": "2026-03-05T10:00:00.000+09:00"
}
인증: api-key 헤더, 값은 환경변수 BREVO_API_KEY
Response (성공):
{
"id": 12345
}
F5-3. GetRates (SNS 배포)
현재 상태: GetRates API 문서 미확인. CEO에게 확인 필요.
예상 구조 (확인 후 수정 필요):
POST https://api.getrates.com/v1/posts
{
"platforms": ["twitter", "instagram"],
"content": "AI 도구로 업무 효율 200% 올리는 방법\n\n#AI #생산성 #자동화",
"media_urls": [],
"scheduled_at": "2026-03-05T11:00:00+09:00"
}
블로킹: GetRates API 문서 + API 키가 필요. CEO 확인 전까지 구현 보류.
F6. 배포 결과 추적
목적: 각 배포 시도의 성공/실패를 기록하고, 대시보드에서 확인할 수 있도록 한다.
F6-1. 배포 결과 대시보드
콘텐츠 상세 페이지 하단에 배포 이력을 표시한다.
+------------------------------------------------------------------+
| 배포 이력 |
+------------------------------------------------------------------+
| |
| #1 blog.apppro.kr [성공] 2026-03-05 09:01 |
| URL: https://apppro.kr/blog/ai-tools-productivity |
| |
| #2 brevo [성공] 2026-03-05 10:02 |
| 캠페인 ID: 12345 |
| |
| #3 getrates [실패] 2026-03-05 11:00 |
| 에러: API key invalid [재시도] |
| |
+------------------------------------------------------------------+
F6-2. 전체 배포 로그 페이지
기존 /[project]/logs 페이지에 publish_logs 탭을 추가한다.
| 필터 | 값 |
|---|---|
| 플랫폼 | 전체 / blog.apppro.kr / brevo / getrates |
| 상태 | 전체 / success / failed / retrying |
| 기간 | 최근 7일 / 30일 / 전체 |
F6-3. 통계 요약
analytics 페이지에 배포 성공률 차트 추가:
- 플랫폼별 성공/실패 비율 (파이 차트)
- 일별 배포 건수 (라인 차트)
- 평균 배포 소요 시간
4. API 설계
4-1. content-orchestration 내부 API Routes
(1) POST /api/content — 콘텐츠 생성
// Request
{
project: string; // 'apppro'
type: string; // 'blog' | 'newsletter' | 'sns'
title: string;
content_body: string; // Markdown
pillar?: string;
channel?: string;
metadata?: {
seo_title?: string;
seo_description?: string;
tags?: string[];
};
}
// Response
{
success: true;
id: string; // 생성된 content_queue ID
}
(2) PUT /api/content/[id] — 콘텐츠 수정
// Request
{
title?: string;
content_body?: string;
pillar?: string;
channel?: string;
metadata?: object;
platform_targets?: PlatformTarget[];
}
// Response
{
success: true;
updated_at: number;
}
(3) POST /api/content/[id]/publish — 수동 즉시 배포
// Request
{
platforms?: string[]; // 미지정 시 platform_targets 전체
}
// Response
{
success: true;
results: {
platform_id: string;
status: 'success' | 'failed';
published_url?: string;
error?: string;
}[];
}
(4) POST /api/content/[id]/schedule — 예약 설정
// Request
{
platform_targets: {
platform_id: string;
scheduled_at: number; // Unix ms
config?: object;
}[];
}
// Response
{
success: true;
status: 'scheduled';
}
(5) GET /api/cron/publish — Cron 스케줄러 (Vercel Cron)
// 인증: CRON_SECRET 환경변수 검증
// 매 5분 실행
// 내부 로직:
// 1. scheduled content 조회
// 2. 시각 도달 항목 → 플랫폼별 배포
// 3. publish_logs 기록
// 4. status 업데이트
// Response
{
processed: number;
results: { content_id: string; platform: string; status: string }[];
}
(6) POST /api/content/[id]/retry — 실패 항목 재시도
// Request
{
platform_id: string; // 재시도할 플랫폼
log_id: string; // publish_logs ID
}
// Response
{
success: boolean;
status: 'success' | 'failed';
published_url?: string;
error?: string;
}
(7) GET /api/publish-logs — 배포 로그 조회
// Query params
?content_id=xxx // 특정 콘텐츠
&platform_id=brevo // 특정 플랫폼
&status=failed // 특정 상태
&limit=50
// Response
{
logs: PublishLog[];
total: number;
}
4-2. apppro.kr 프로젝트에 구축 필요한 API
POST /api/blog/publish (apppro-kr 프로젝트)
// 인증: x-api-key 헤더
// 위치: projects/apppro-kr/src/app/api/blog/publish/route.ts
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs/promises';
import path from 'path';
export async function POST(req: NextRequest) {
// 1. API 키 검증
const apiKey = req.headers.get('x-api-key');
if (apiKey !== process.env.BLOG_PUBLISH_API_KEY) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 });
}
// 2. 요청 파싱
const { title, slug, content, category, tags, seo_title, seo_description, published } = await req.json();
// 3. .mdx 파일 생성 (content/blog/ 디렉토리)
const frontmatter = `---
title: "${title}"
date: "${new Date().toISOString()}"
category: "${category || 'general'}"
tags: [${(tags || []).map((t: string) => `"${t}"`).join(', ')}]
seo_title: "${seo_title || title}"
description: "${seo_description || ''}"
published: ${published ?? true}
---`;
const fileContent = `${frontmatter}\n\n${content}`;
const filePath = path.join(process.cwd(), 'content', 'blog', `${slug}.mdx`);
// 4. 파일 쓰기
await fs.writeFile(filePath, fileContent, 'utf-8');
// 5. 응답
return NextResponse.json({
success: true,
slug,
url: `https://apppro.kr/blog/${slug}`,
published_at: new Date().toISOString(),
});
}
주의: 파일 기반 블로그에서 API 게시 후 실제 반영되려면 Vercel 재배포가 필요하다.
- 해결 방안 1: API 게시 후 Vercel Deploy Hook 트리거 (가장 현실적)
- 해결 방안 2: ISR (Incremental Static Regeneration) 활용
- 해결 방안 3: DB 기반 블로그 전환 (장기)
Vercel Deploy Hook 활용:
// 게시 성공 후 Deploy Hook 호출
await fetch(process.env.VERCEL_DEPLOY_HOOK_URL!, { method: 'POST' });
4-3. Server Actions (기존 확장)
src/app/actions/content.ts 에 추가:
// 기존
export async function approveContent(id, projectId) { ... }
export async function rejectContent(id, projectId, reason) { ... }
export async function moveToReview(id, projectId) { ... }
export async function scheduleContent(id, projectId, scheduledAt) { ... }
// 추가
export async function createContent(formData: FormData) { ... }
export async function updateContent(id: string, formData: FormData) { ... }
export async function publishNow(id: string, projectId: string) { ... }
export async function retryPublish(id: string, logId: string, platformId: string) { ... }
export async function cancelSchedule(id: string, projectId: string) { ... }
export async function moveToDraft(id: string, projectId: string) { ... }
export async function setPlatformTargets(id: string, targets: PlatformTarget[]) { ... }
5. 구현 우선순위
Phase 1: 콘텐츠 에디터 + 스케줄러 UI (기반 인프라)
| 순번 | 작업 | 세부 |
|---|---|---|
| 1-1 | DB 스키마 마이그레이션 | platform_targets, publish_results, metadata 컬럼 추가 |
| 1-2 | platform_configs 테이블 생성 | 초기 데이터 INSERT |
| 1-3 | publish_logs 테이블 생성 | 인덱스 포함 |
| 1-4 | 콘텐츠 생성 페이지 | /[project]/content/new |
| 1-5 | 콘텐츠 상세/편집 페이지 | /[project]/content/[id] + Markdown 에디터 |
| 1-6 | Server Actions 확장 | createContent, updateContent, moveToDraft |
| 1-7 | 플랫폼 선택 + 스케줄 UI | Datetime picker + 체크박스 |
| 1-8 | 상태 전이 로직 강화 | 유효하지 않은 전이 차단 |
CEO 블로킹: 없음 (순수 프론트엔드 + DB 작업) 의존성: 없음 산출물: 콘텐츠 CRUD + 예약 설정 가능한 대시보드
Phase 2: Brevo 이메일 연동 (가장 Ready)
| 순번 | 작업 | 세부 |
|---|---|---|
| 2-1 | Brevo 퍼블리셔 모듈 | src/lib/publishers/brevo.ts |
| 2-2 | 즉시 배포 Server Action | publishNow → Brevo API 호출 |
| 2-3 | Cron Job 구현 | /api/cron/publish + vercel.json |
| 2-4 | publish_logs 기록 로직 | 성공/실패 로그 저장 |
| 2-5 | 배포 결과 UI | 콘텐츠 상세에 로그 표시 |
| 2-6 | 재시도 기능 | retryPublish Server Action |
CEO 블로킹: BREVO_API_KEY (이미 설정됨, 확인 필요) 의존성: Phase 1 완료 산출물: Brevo를 통한 이메일 자동 발송 가능
Phase 3: apppro.kr 블로그 연동
| 순번 | 작업 | 세부 |
|---|---|---|
| 3-1 | apppro.kr에 /api/blog/publish 구현 | apppro-kr 프로젝트 작업 필요 |
| 3-2 | Vercel Deploy Hook 설정 | API 게시 후 재배포 트리거 |
| 3-3 | 블로그 퍼블리셔 모듈 | src/lib/publishers/apppro-blog.ts |
| 3-4 | Cron Job에 블로그 배포 추가 | 기존 cron 확장 |
| 3-5 | slug 자동 생성 로직 | 한글 제목 → 영문 slug 변환 |
CEO 블로킹: APPPRO_BLOG_API_KEY 생성 + Deploy Hook URL 의존성: Phase 1 완료 + apppro-kr 프로젝트 API 구축 산출물: 대시보드에서 블로그 포스트 직접 게시
Phase 4: GetRates / SNS 연동
| 순번 | 작업 | 세부 |
|---|---|---|
| 4-1 | GetRates API 문서 확인 | CEO에게 API 문서 요청 |
| 4-2 | SNS 퍼블리셔 모듈 | src/lib/publishers/getrates.ts |
| 4-3 | SNS 콘텐츠 포맷터 | 플랫폼별 글자 수 제한, 해시태그 |
| 4-4 | 미디어 첨부 기능 | 이미지 URL 포함 배포 |
| 4-5 | Cron Job에 SNS 배포 추가 | 기존 cron 확장 |
CEO 블로킹: GetRates API 문서 + API 키 + 계정 접근 의존성: Phase 1 완료 + GetRates API 문서 확보 산출물: SNS 자동 배포
6. CEO 블로킹 항목
즉시 필요 (Phase 1 착수 전)
| 항목 | 설명 | 긴급도 |
|---|---|---|
| Vercel production branch 설정 | content-orchestration Vercel 프로젝트에서 production branch 설정 필요 | 높음 |
Phase 2 착수 전 필요
| 항목 | 설명 | 긴급도 |
|---|---|---|
| BREVO_API_KEY 확인 | 이미 설정되어 있으나 content-orchestration 환경에서 사용 가능한지 확인 | 중간 |
| Brevo 발신자 인증 | contact@apppro.kr 인증 완료 확인 | 중간 |
| Brevo Contact List ID | 대상 구독자 리스트 번호 확인 | 중간 |
Phase 3 착수 전 필요
| 항목 | 설명 | 긴급도 |
|---|---|---|
| APPPRO_BLOG_API_KEY 생성 | apppro.kr에서 사용할 API 인증키 생성 | 중간 |
| Vercel Deploy Hook URL | apppro.kr 프로젝트의 Deploy Hook 생성 | 중간 |
Phase 4 착수 전 필요
| 항목 | 설명 | 긴급도 |
|---|---|---|
| GetRates API 문서 | GetRates 서비스의 API 엔드포인트, 인증 방식, 요청/응답 스펙 | 높음 |
| GetRates API 키 | 서비스 API 인증키 | 높음 |
| GetRates 계정 접근 | 연동할 SNS 계정 (Twitter, Instagram 등) 설정 여부 | 높음 |
확인 사항 (블로킹은 아님)
| 항목 | 설명 |
|---|---|
| CRON_SECRET | Vercel Cron Job 인증용 시크릿 (자동 생성 가능) |
| 콘텐츠 품질 기준 | CEO가 직접 승인할 것인지, VP 승인으로 충분한지 |
| 배포 빈도 제한 | 일일/주간 최대 배포 건수 제한 여부 |
7. 예상 구현 공수
Phase별 예상 공수
| Phase | 내용 | 신규 파일 | 수정 파일 | 비고 |
|---|---|---|---|---|
| Phase 1 | 에디터 + 스케줄러 UI | 3~4개 | 3~4개 | CEO 블로킹 없음, 즉시 착수 가능 |
| Phase 2 | Brevo 연동 | 2~3개 | 2~3개 | BREVO_API_KEY 확인 후 착수 |
| Phase 3 | apppro.kr 블로그 연동 | 2 | 2~3개 | 크로스 프로젝트 작업 필요 |
| Phase 4 | GetRates SNS 연동 | 2~3개 | 1~2개 | API 문서 확보 후 착수 |
기술 의존성 추가
| 패키지 | 용도 | Phase |
|---|---|---|
react-markdown | Markdown 미리보기 | Phase 1 |
remark-gfm | GFM 지원 | Phase 1 |
uuid 또는 crypto.randomUUID() | publish_logs ID 생성 | Phase 1 |
(기존 @libsql/client) | Turso DB | 이미 설치됨 |
리스크 요소
| 리스크 | 영향도 | 대응 |
|---|---|---|
| apppro.kr 파일 기반 블로그의 API 게시 한계 | 중간 | Deploy Hook으로 우회, 장기적으로 DB 전환 검토 |
| GetRates API 미확인 | 높음 | Phase 4로 분리, CEO 확인 후 설계 수정 |
| Vercel Cron Free 플랜 제한 | 낮음 | Free 플랜도 cron 지원, 최소 간격 확인 필요 |
| 콘텐츠 대량 발행 시 Brevo 일일 한도 | 중간 | Brevo Free: 300/일, 초과 시 큐잉 처리 |
부록: 현재 파일 구조 및 변경 대상
현재 파일 구조
content-orchestration/src/
├── app/
│ ├── layout.tsx
│ ├── page.tsx // 전체 프로젝트 요약
│ ├── globals.css
│ ├── actions/
│ │ └── content.ts // Server Actions (수정 대상)
│ └── [project]/
│ ├── layout.tsx // 네비게이션 (수정 대상)
│ ├── page.tsx // 프로젝트 대시보드
│ ├── content/
│ │ └── page.tsx // 콘텐츠 워크플로우 (수정 대상)
│ ├── calendar/
│ │ └── page.tsx
│ ├── rss/
│ │ └── page.tsx
│ ├── logs/
│ │ └── page.tsx // (수정 대상: publish_logs 탭 추가)
│ └── analytics/
│ └── page.tsx // (수정 대상: 배포 통계 추가)
├── components/
│ └── charts.tsx
└── lib/
├── content-db.ts // DB 함수 (수정 대상)
├── projects.ts
├── rss-feeds.ts
└── sns-collector.ts
추가될 파일
content-orchestration/src/
├── app/
│ ├── api/
│ │ ├── content/
│ │ │ └── route.ts // Phase 1: 콘텐츠 CRUD API
│ │ ├── cron/
│ │ │ └── publish/
│ │ │ └── route.ts // Phase 2: 스케줄러 Cron
│ │ └── publish-logs/
│ │ └── route.ts // Phase 2: 로그 조회 API
│ └── [project]/
│ └── content/
│ ├── new/
│ │ └── page.tsx // Phase 1: 콘텐츠 생성
│ └── [id]/
│ └── page.tsx // Phase 1: 콘텐츠 상세/편집
├── components/
│ ├── content-editor.tsx // Phase 1: Markdown 에디터 (client)
│ ├── platform-selector.tsx // Phase 1: 플랫폼 선택 UI (client)
│ ├── schedule-picker.tsx // Phase 1: 예약 시각 선택 (client)
│ └── publish-log-list.tsx // Phase 2: 배포 로그 표시
└── lib/
└── publishers/
├── index.ts // 퍼블리셔 인터페이스 + 팩토리
├── brevo.ts // Phase 2: Brevo 이메일 퍼블리셔
├── apppro-blog.ts // Phase 3: apppro.kr 블로그 퍼블리셔
└── getrates.ts // Phase 4: GetRates SNS 퍼블리셔
퍼블리셔 인터페이스 설계
// src/lib/publishers/index.ts
export interface PublishRequest {
content_id: string;
title: string;
content_body: string; // Markdown
metadata: Record<string, unknown>;
platform_config: Record<string, unknown>;
}
export interface PublishResult {
success: boolean;
published_url?: string;
external_id?: string; // 플랫폼 측 ID (Brevo campaign ID 등)
error?: string;
response_status?: number;
}
export interface Publisher {
platform_id: string;
publish(request: PublishRequest): Promise<PublishResult>;
validateConfig(config: Record<string, unknown>): boolean;
}
export function getPublisher(platformId: string): Publisher {
switch (platformId) {
case 'brevo': return new BrevoPublisher();
case 'blog.apppro.kr': return new AppProBlogPublisher();
case 'getrates': return new GetRatesPublisher();
default: throw new Error(`Unknown platform: ${platformId}`);
}
}
결론
Phase 1은 CEO 블로킹 없이 즉시 착수 가능하며, 콘텐츠 에디터와 스케줄러 UI를 구축하면 CEO가 대시보드에서 직접 콘텐츠를 관리할 수 있는 기반이 완성된다.
Phase 2 (Brevo)는 이미 다른 프로젝트에서 연동 경험이 있어 빠르게 구현 가능하다.
Phase 3-4는 크로스 프로젝트 작업 (apppro-kr API 구축)과 외부 서비스 API 확보 (GetRates)가 필요하므로 선행 조건 해결 후 순차 진행한다.
권장 착수 순서: Phase 1 즉시 시작 → Phase 2 병행 준비 → CEO 블로킹 해제 순서대로 Phase 3-4 진행.