← 목록으로
2026-03-01plans

콘텐츠 오케스트레이션 완전체 — 시스템 설계 기획서

버전: v1.0 작성일: 2026-03-01 작성자: Product Architect PL 검토 대상: VP (머스크), CEO (김준영 대표) 프로젝트: content-orchestration (https://content-orchestration.vercel.app)


목차

  1. 시스템 아키텍처 개요
  2. DB 스키마 변경사항
  3. 기능 명세 (Feature Spec)
  4. API 설계
  5. 구현 우선순위 (Phase 계획)
  6. CEO 블로킹 항목
  7. 예상 구현 공수

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.krslug (자동 생성 또는 수동), category, tags
brevosubject (이메일 제목), list_id, sender
getratesSNS 플랫폼 선택 (twitter/instagram/linkedin), 해시태그

F4. 자동 배포 트리거

목적: 승인된 콘텐츠를 예약 시각에 맞춰 자동으로 배포하거나, 수동으로 즉시 배포한다.

F4-1. 트리거 방식

방식조건구현
예약 자동 배포status='scheduled' AND scheduled_at <= nowVercel 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 로직:

  1. content_queue에서 status='scheduled' AND scheduled_at <= Date.now() 조회
  2. 각 콘텐츠의 platform_targets 파싱
  3. 해당 시각이 도달한 플랫폼별로 배포 API 호출
  4. publish_logs 기록
  5. 모든 플랫폼 완료 시 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-1DB 스키마 마이그레이션platform_targets, publish_results, metadata 컬럼 추가
1-2platform_configs 테이블 생성초기 데이터 INSERT
1-3publish_logs 테이블 생성인덱스 포함
1-4콘텐츠 생성 페이지/[project]/content/new
1-5콘텐츠 상세/편집 페이지/[project]/content/[id] + Markdown 에디터
1-6Server Actions 확장createContent, updateContent, moveToDraft
1-7플랫폼 선택 + 스케줄 UIDatetime picker + 체크박스
1-8상태 전이 로직 강화유효하지 않은 전이 차단

CEO 블로킹: 없음 (순수 프론트엔드 + DB 작업) 의존성: 없음 산출물: 콘텐츠 CRUD + 예약 설정 가능한 대시보드

Phase 2: Brevo 이메일 연동 (가장 Ready)

순번작업세부
2-1Brevo 퍼블리셔 모듈src/lib/publishers/brevo.ts
2-2즉시 배포 Server ActionpublishNow → Brevo API 호출
2-3Cron Job 구현/api/cron/publish + vercel.json
2-4publish_logs 기록 로직성공/실패 로그 저장
2-5배포 결과 UI콘텐츠 상세에 로그 표시
2-6재시도 기능retryPublish Server Action

CEO 블로킹: BREVO_API_KEY (이미 설정됨, 확인 필요) 의존성: Phase 1 완료 산출물: Brevo를 통한 이메일 자동 발송 가능

Phase 3: apppro.kr 블로그 연동

순번작업세부
3-1apppro.kr에 /api/blog/publish 구현apppro-kr 프로젝트 작업 필요
3-2Vercel Deploy Hook 설정API 게시 후 재배포 트리거
3-3블로그 퍼블리셔 모듈src/lib/publishers/apppro-blog.ts
3-4Cron Job에 블로그 배포 추가기존 cron 확장
3-5slug 자동 생성 로직한글 제목 → 영문 slug 변환

CEO 블로킹: APPPRO_BLOG_API_KEY 생성 + Deploy Hook URL 의존성: Phase 1 완료 + apppro-kr 프로젝트 API 구축 산출물: 대시보드에서 블로그 포스트 직접 게시

Phase 4: GetRates / SNS 연동

순번작업세부
4-1GetRates API 문서 확인CEO에게 API 문서 요청
4-2SNS 퍼블리셔 모듈src/lib/publishers/getrates.ts
4-3SNS 콘텐츠 포맷터플랫폼별 글자 수 제한, 해시태그
4-4미디어 첨부 기능이미지 URL 포함 배포
4-5Cron 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 URLapppro.kr 프로젝트의 Deploy Hook 생성중간

Phase 4 착수 전 필요

항목설명긴급도
GetRates API 문서GetRates 서비스의 API 엔드포인트, 인증 방식, 요청/응답 스펙높음
GetRates API 키서비스 API 인증키높음
GetRates 계정 접근연동할 SNS 계정 (Twitter, Instagram 등) 설정 여부높음

확인 사항 (블로킹은 아님)

항목설명
CRON_SECRETVercel Cron Job 인증용 시크릿 (자동 생성 가능)
콘텐츠 품질 기준CEO가 직접 승인할 것인지, VP 승인으로 충분한지
배포 빈도 제한일일/주간 최대 배포 건수 제한 여부

7. 예상 구현 공수

Phase별 예상 공수

Phase내용신규 파일수정 파일비고
Phase 1에디터 + 스케줄러 UI3~4개3~4개CEO 블로킹 없음, 즉시 착수 가능
Phase 2Brevo 연동2~3개2~3개BREVO_API_KEY 확인 후 착수
Phase 3apppro.kr 블로그 연동23개 (content-orchestration) + 12개 (apppro-kr)2~3개크로스 프로젝트 작업 필요
Phase 4GetRates SNS 연동2~3개1~2개API 문서 확보 후 착수

기술 의존성 추가

패키지용도Phase
react-markdownMarkdown 미리보기Phase 1
remark-gfmGFM 지원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 진행.

plans/2026/03/01/content-orchestration-full-spec.md