# Slack → Notion 콘텐츠 파이프라인 (사례 연구)

Slack에 링크 하나를 던지면, 원문을 읽고 한국어 콘텐츠 기획을 만든 뒤 Notion DB 페이지 생성·본문 작성·이미지 삽입·완료 보고까지 자동으로 처리하는 **에이전트 기반 콘텐츠 생산 파이프라인**입니다.

이 저장소는 실제로 운영한 자동화를 **교육/사례 공유 목적**으로 정리한 자료입니다. 특정 조직 전용 값은 모두 플레이스홀더로 치환돼 있어, 구조와 설계 원칙을 그대로 학습하거나 자신의 환경에 맞춰 재구성할 수 있습니다.

> 📄 시각적으로 정리된 설명 페이지: **[`index.html`](./index.html)** — 브라우저로 바로 열거나 GitHub Pages로 배포해 보세요.

## 한 줄 요약

Slack에서 `@봇 + URL`을 받으면 → **원문 수집 → 한글 콘텐츠 기획(JSON) → Notion 페이지 생성 + 본문 작성 + AI 이미지 삽입 → Slack 완료 보고**까지 사람의 개입 없이 처리합니다.

## 왜 이렇게 만들었나

"쓸만한 링크를 발견하는 일(큐레이션)"은 사람이 잘하는 일이고, "그것을 일정한 포맷의 콘텐츠로 정리하는 일"은 반복되는 공정입니다. 이 시스템은 후자를 자동화해서, 사람은 어떤 링크를 콘텐츠화할지 고르는 데만 집중하게 합니다.

그래서 이 에이전트는 "요약봇"보다 **콘텐츠 공장 작업자**에 가깝습니다.

## LLM을 쓰면서도 신뢰성을 지키는 법 (이 사례의 핵심)

모델에 프롬프트를 던져 자유롭게 답하게 하면, 매번 형식과 품질이 달라져 **시스템으로 신뢰할 수 없습니다.** 이 파이프라인이 신뢰성을 확보하는 방법은 두 가지입니다.

1. **LLM의 출력마저 JSON으로 구조화한다.** 자유 서술 대신 고정된 스키마(`plan.json`)의 칸을 채우게 합니다. 출력이 구조화돼 있으면 기계가 검증할 수 있습니다 — TL;DR 정확히 5개, 저자 규칙, 한글 포함을 코드가 강제하고 어기면 `VALIDATION_FAILED`로 중단합니다.
2. **나머지는 전부 코드와 CLI로 감싼다.** 원문 수집·검증·저장·이미지·보고는 결정론적 스크립트가 담당합니다. LLM 판단은 7단계 중 딱 한 곳(콘텐츠 기획)뿐입니다.

> 즉, LLM을 "자유로운 작가"가 아니라 "정해진 칸을 채우는 부품"으로 씁니다. 판단이 필요한 **좁은 구간만** 모델에 맡기고, 그 입출력을 구조화된 데이터로 고정한 뒤, 넓은 나머지 영역을 코드/CLI가 책임집니다. 신뢰성은 여기서 나옵니다.

```text
원문 수집          │ 콘텐츠 기획   │ 검증 · 저장 · 이미지 · 보고
web_fetch · CLI    │ LLM → JSON    │ 코드 · Notion API · git · CLI
───────────────────┼───────────────┼──────────────────────────────
      결정론적      │  좁은 LLM 구간 │            결정론적
```

## 무엇으로 만들었나 (도구는 선택 가능)

본격적인 흐름에 들어가기 전에, 이 시스템이 어떤 부품으로 구성되는지 지도부터 봅니다. 아래는 이 사례가 실제로 쓴 도구일 뿐 **정답이 아닙니다.** 중요한 건 각 레이어의 *역할*이고, 도구는 팀 환경·비용·이미 쓰는 인프라에 맞춰 바꿔 끼울 수 있습니다.

| 레이어 | 이 사례에서 쓴 것 | 대체 가능한 방식 |
|---|---|---|
| 트리거 / 보고 | Slack (봇 멘션 + 스레드 댓글) | Discord · Telegram · 이메일 · 웹훅 · CLI 직접 실행 · GitHub Issue |
| 런타임 | Node.js (의존성 없는 `.mjs`) | Python · Deno · Bash · 서버리스 함수(Lambda 등) |
| 원문 수집 | `web_fetch` · Firecrawl | Playwright/Puppeteer · Jina Reader · Readability · `curl`+파서 · yt-dlp(영상 자막) |
| 콘텐츠 기획 | LLM (코딩 에이전트) | Claude · GPT · Gemini · 로컬 모델(Ollama) — 단, 출력은 구조화(JSON)로 고정 |
| 저장소 | Notion API (DB 페이지 + 블록) | Google Docs/Sheets · Obsidian · 마크다운+Git · Airtable · WordPress |
| 이미지 생성 | Gemini 이미지 모델 → GitHub raw URL | DALL·E · Stable Diffusion · 이미지 없음(텍스트만) · S3/Cloudinary 호스팅 |

> **고르는 기준**은 유행이 아니라 *비용 · 신뢰성 · 재현성 · 이미 쓰는 도구인지*입니다. 레이어의 경계(계약)만 지키면 한 조각을 교체해도 나머지는 그대로 돌아갑니다.

## 트리거

Slack 채널에서 봇을 멘션하고 URL을 붙이면 실행됩니다.

```text
<@YOUR_BOT_ID> https://example.com/article
```

실행 조건은 두 가지이며, 맞지 않으면 조용히 무시(`NO_REPLY`)합니다.

- 메시지에 봇 멘션(`<@YOUR_BOT_ID>`)이 있어야 합니다.
- 메시지에 `http(s)://` URL이 최소 1개 있어야 합니다.

## 동작 흐름 (7단계)

LLM 판단은 **3단계(콘텐츠 기획) 한 곳에만** 있고, 나머지는 전부 결정론적 스크립트입니다.

1. **Slack 컨텍스트 추출** — 채널 ID·스레드 ts를 세션 상태에서 읽어옵니다.
2. **원문 수집** — 일반 URL은 `web_fetch`, 추출이 불안정한 도메인은 Firecrawl로 폴백.
3. **콘텐츠 기획 JSON 생성** *(유일한 LLM 단계)* — 제목·요약·TL;DR·섹션·이미지 아이디어를 구조화.
4. **JSON 검증·정리** — 제어문자 제거 + 파싱 가능 여부 확인.
5. **오케스트레이션** — Slack ctx 로드 + plan 전달 + 재시도 + 실패 보고를 묶은 wrapper.
6. **Notion 반영** — 중복 체크 → 페이지 생성 → 본문 작성 → 소요시간 기록 → Slack 보고. (LLM 호출 없음)
7. **AI 이미지 삽입** — 플레이스홀더를 실제 생성 이미지로 교체.

에이전트 자신의 최종 응답은 항상 `NO_REPLY` 한 줄이며, Slack에는 완료 보고만 남습니다.

## 콘텐츠 기획 JSON 스키마

LLM과 스크립트 사이의 **데이터 계약**입니다. 이 계약이 고정돼 있어 생성 단계(LLM)와 반영 단계(코드)를 분리할 수 있습니다.

```json
{
  "title_ko": "한글 제목",
  "author": "원문 기준 저자 (불명 시 '알수없음')",
  "category": "카테고리",
  "keywords": ["키워드"],
  "summary_ko": "한글 3-5문장 요약",
  "tldr_bullets": ["정확히 5개"],
  "sections": [
    {
      "h1": "섹션 제목",
      "image_placeholders": ["이미지 프롬프트"],
      "toggle_title": "토글 제목",
      "bullets": ["본문 bullet"]
    }
  ]
}
```

품질 기준(코드로 강제되는 하드 룰):

- DB 필드와 본문은 기본적으로 한국어입니다.
- `Author`는 Slack 요청자가 아니라 **원문 저자**를 사용합니다. 확인 불가 시 `알수없음`입니다.
- TL;DR은 정확히 5개, 섹션은 최소 2개입니다.
- 이미지 플레이스홀더는 페이지 전체 기준 3~5개입니다.
- 위반 시 `VALIDATION_FAILED`로 중단합니다.

## 코드 구성

핵심 스크립트는 `code/bin/`에 있습니다. 각 스크립트는 한 가지 일만 하고, 셸에서 조합됩니다.

| 파일 | 역할 |
|---|---|
| `read_last_slack_ctx.mjs` | 세션 상태에서 Slack channel id, thread ts를 가져옵니다. |
| `firecrawl_scrape_markdown.mjs` | Firecrawl 결과에서 markdown 본문만 안전하게 추출합니다. |
| `json_minify_sanitize.mjs` | plan JSON의 파싱 가능성을 검증하고 minify/sanitize합니다. |
| `run_notion_apply_from_files.mjs` | Slack ctx 로드·plan 전달·재시도·실패 보고를 묶은 오케스트레이터입니다. |
| `notion_apply.mjs` | Notion 페이지 생성/스킵/본문 작성/Slack 보고의 중심 스크립트입니다. |
| `notion_images_replace.mjs` | 이미지 플레이스홀더를 실제 생성 이미지로 바꾸고 Notion에 삽입합니다. |

## 생성 사례

`examples/plans/`에 실제로 생성된 기획 JSON 8건이 들어 있습니다.

| 파일 | 제목 | 카테고리 | 섹션 | 이미지 |
|---|---|---|---:|---:|
| `plan_ai_intensifies_work.json` | AI는 일을 줄이지 않는다: 오히려 일을 '진하게' 만든다 | AI/업무혁신 | 4 | 4 |
| `plan_brunch_230kimi_47.json` | Anthropic은 왜 '바이브'로 굴러가는데도 강해 보일까 | AI/조직문화 | 6 | 5 |
| `plan_compound_engineering.json` | 컴파운드 엔지니어링: 오늘 한 작업이 내일을 더 쉽게 만들게 하는 방법 | 개발/프로덕트 | 5 | 4 |
| `plan_yozm_3468.json` | 회사 AI 도입이 늘 '체감'이 없는 이유 | AI/전략 | 5 | 5 |
| `plan_yozm_3604.json` | 검색 대신 '리서치'가 필요한 시대 | 업무/리서치 | 5 | 5 |
| `plan_yozm_3608.json` | LLM이 코드를 잘 짜도 개발자를 대체 못 하는 이유 | AI/개발인사이트 | 4 | 4 |
| `plan_yozm_3609.json` | '읽지 않은 코드도 배포한다'는 선언 | AI/개발문화 | 5 | 4 |
| `plan_yozm_3610.json` | 클로드 코드, '잘 쓰는 사람들'은 무엇을 다르게 하나 | AI/업무도구 | 4 | 4 |

가장 좋은 참고 사례는 `plan_yozm_3609.json`입니다. 원문을 단순 요약하지 않고 제목·TL;DR·섹션·이미지 프롬프트까지 갖춘 Notion용 콘텐츠로 재구성한 형태가 잘 드러납니다.

## 이 사례에서 배울 점

다른 자동화에도 그대로 적용되는 설계 원칙들입니다.

- **역할 분리** — 판단이 필요한 큐레이션은 사람, 반복 공정은 에이전트.
- **Code-First** — LLM 판단은 꼭 필요한 접합부(콘텐츠 기획)에만. 나머지는 결정론적 코드로 재현 가능·디버깅 용이.
- **데이터 계약** — LLM과 코드 사이를 고정된 JSON 스키마로 연결.
- **가드레일** — 품질 규칙을 문서 권고가 아니라 실행 코드로 강제.
- **멱등성** — 같은 URL은 스킵하고 업데이트도 막아 중복 방지.
- **함정 기록** — zsh 따옴표 깨짐, `jq` 제어문자 크래시 같은 실패를 우회로 끝내지 않고 근본 원인과 대응을 코드·문서에 남김.

## 사전 설정 (환경변수 / 플레이스홀더)

이 저장소의 값은 모두 플레이스홀더입니다. 실제 실행에는 아래를 설정하세요.

| 항목 | 설정 방법 | 설명 |
|---|---|---|
| Notion API Key | `NOTION_API_KEY` env 또는 `~/.config/notion/api_key` | Notion 통합 토큰 |
| Notion DB ID | `NOTION_DB_ID` env 또는 `--dbid` 인자 | 대상 콘텐츠 DB |
| Slack Bot ID | 트리거 멘션 `<@YOUR_BOT_ID>` 를 실제 봇 유저 ID로 교체 | 예: `<@U01234567>` |
| Slack Bot Token | `SLACK_BOT_TOKEN` env 또는 설정 파일 | 완료 보고용 |
| 이미지 저장 GitHub 레포 | `IMAGES_REPO_OWNER` / `IMAGES_REPO_NAME` / `IMAGES_REPO_BRANCH` env 또는 `--repoOwner` / `--repoName` / `--repoBranch` / `--repoRemote` 인자 | 생성 이미지를 커밋·푸시하고 raw URL로 삽입 |
| Gemini API Key | `GEMINI_API_KEY` env 또는 설정 파일 | 이미지 생성 |

## 대표 실행 명령

콘텐츠 기획 JSON을 만든 뒤 실행하는 wrapper 예시입니다.

```bash
export NOTION_DB_ID="<YOUR_NOTION_DB_ID>"
export IMAGES_REPO_OWNER="<github-user>"

node $HOME/.openclaw/workspace/agents/notion-codex-writer/bin/run_notion_apply_from_files.mjs \
  --dbid "$NOTION_DB_ID" \
  --url "<원문 URL>" \
  --requestedBy "<Slack 표시 이름>" \
  --planFile $HOME/.openclaw/workspace/agents/notion-codex-writer/tmp_plan.json \
  --withImages 1 \
  --imagesMax 5 \
  --imagesRepoPath $HOME/.openclaw/workspace/nxt-ai-content-images
```

주의할 점:

- `slackChannelId`는 `#채널명`이 아니라 `C...` 또는 `G...` 형식이어야 합니다.
- `slackThreadTs`는 `read_last_slack_ctx.mjs`에서 가져온 값을 사용합니다.
- URL 중복이면 기존 페이지를 업데이트하지 않고 스킵합니다.
- 최종 Slack 댓글은 에이전트가 직접 쓰지 않고 apply 단계가 처리합니다.

## 저장소 구조

```
index.html             ← 시각화된 사례 설명 페이지 (교육용)
README.md              ← 이 문서
reference/AGENTS.md    ← 에이전트 운영 지침 (상세 규칙)
code/bin/              ← 핵심 실행 스크립트 6개
examples/plans/        ← 실제 생성된 기획 JSON 8건
```

## 함께 보기

1. [`index.html`](./index.html) — 시각화된 전체 개요
2. `reference/AGENTS.md` — 상세 운영 규칙
3. `examples/plans/plan_yozm_3609.json` — 대표 산출물
4. `code/bin/notion_apply.mjs` — 중심 스크립트
5. `code/bin/notion_images_replace.mjs` — 이미지 파이프라인
