Spec

트렌드 리포트 1단계 구현 가이드 — 24시간 안에 첫 푸시

Notion(저장·검수·열람) + Google Apps Script(자동 수집·LLM 분류) + Telegram 봇(푸시). 코드 한 번만 붙여넣으면 굴러가는 0원형 구성. 스마트폰 = Notion 앱 + Telegram 앱 두 개로 끝납니다. 이 단계가 안정되면 같은 콘텐츠 흐름 위에 PWA를 올릴 수 있어요.

2026-05-04 · Spec · 약 12분 · 1단계 구현 가이드
이 단계의 목표. 콘텐츠 발행 흐름을 먼저 검증합니다. "하루 5~7건의 큐레이션을 매일 받고, 매달 1편의 리포트를 받는 일상"이 본인에게 실제로 잘 맞는지를 1~2주 안에 확인. 잘 맞으면 PWA, 안 맞으면 흐름 자체를 조정.
00 Prerequisites

준비물 (체크리스트)

Notion 계정
무료. 모바일 앱 설치 (iOS/Android).
Google 계정
Apps Script 무료 사용. Gmail/Google Drive 계정 그대로.
Telegram 계정
무료. 모바일 앱 설치.
Anthropic API Key
Claude Haiku 4.5 사용. 월 약 $5~15. console.anthropic.com
Notion Internal Integration Token
비용 요약. Claude Haiku는 매우 저렴합니다. 하루 30~50건 분류 + 월·분기 리포트 초안 생성을 합쳐도 한 달 $5~15 사이. 그 외 모두 무료.
01 Step 1 · Notion

Step 1 — Notion DB 만들기 (10분)

Notion에 새 페이지 → "Table" 데이터베이스 생성. 이름은 "Trend Inbox"로. 아래 컬럼 그대로 만들어 주세요.

컬럼타입용도
TitleTitle기사 제목
URLURL원문 링크
SourceSelect출처 (GWI / Vogue Korea / Elle 등)
CategorySelect리트릿 / F&B / 공간 / 문화·체험 / 기타
ScoreNumber중요도 1~10 (LLM 자동 점수)
SummaryText1줄 요약 (LLM 자동)
StatusSelectcandidate / published / archived
CommentText본인 한 줄 코멘트 (검수 시 추가)
FetchedAtDate수집 일시
PublishedAtDate발행 일시 (있을 때)

Notion 통합 토큰 얻기

  1. notion.so/my-integrations → "New integration" → 이름 "Trend Bot" → 워크스페이스 선택 → 생성.
  2. "Internal Integration Token" 복사 (secret_…). 안전하게 보관.
  3. 방금 만든 Trend Inbox 페이지 → 우측 상단 ⋯ → "Add connections" → "Trend Bot" 추가.
  4. DB의 URL에서 ?v=… 앞 32자 hex가 Database ID. 메모해 두세요.
02 Step 2 · Telegram

Step 2 — Telegram 봇 만들기 (5분)

  1. Telegram에서 @BotFather 검색 → /newbot → 이름 입력 (예: "Sqnc Trend Bot").
  2. 봇 토큰 받기 (123456:ABC-DEF…). 보관.
  3. 방금 만든 봇과의 채팅을 시작 (/start).
  4. 본인의 Chat ID 얻기: 브라우저에서 https://api.telegram.org/bot<토큰>/getUpdates 열기 → "chat":{"id": 12345678}의 숫자 복사.
토큰 관리. Bot Token, Notion Token, Anthropic API Key는 절대 깃허브·메신저·문서에 평문으로 두지 마세요. Apps Script의 PropertiesService에 저장하는 게 안전합니다 (다음 단계에서 처리).
03 Step 3 · Apps Script

Step 3 — Google Apps Script 자동 수집 (30분)

  1. script.google.com → "New project".
  2. 이름: "Trend Collector".
  3. 좌측 ⚙ "Project Settings" → 아래 "Script Properties"에 다음 4개 추가:
    • NOTION_TOKEN — Notion 통합 토큰
    • NOTION_DB_ID — Trend Inbox DB ID
    • ANTHROPIC_API_KEY — Claude API 키
    • TELEGRAM_TOKEN · TELEGRAM_CHAT_ID — 봇 토큰·Chat ID

RSS 소스 (Code.gs 상단)

여기에 본인이 받고 싶은 매체를 넣습니다. 시작용 10개 추천.

Code.gs — RSS sources
const SOURCES = [
  { name: 'GWI',         url: 'https://globalwellnessinstitute.org/feed/' },
  { name: 'GWS Trends',  url: 'https://www.globalwellnesssummit.com/feed/' },
  { name: 'Vogue Korea', url: 'https://www.vogue.co.kr/feed/' },
  { name: 'Elle Korea',  url: 'https://www.elle.co.kr/rss/all.xml' },
  { name: 'W Korea',     url: 'https://www.wkorea.com/feed/' },
  { name: 'Bazaar Korea',url: 'https://www.harpersbazaar.co.kr/feed/' },
  { name: 'Well+Good',   url: 'https://www.wellandgood.com/feed/' },
  { name: 'Goop',        url: 'https://goop.com/feed/' },
  { name: 'Longevity',   url: 'https://longevity.technology/feed/' },
  { name: '롱블랙',      url: 'https://www.longblack.co/feed' },
];

핵심 함수 — fetchAndClassify()

매일 새벽 1회 실행되어 RSS 수집 → Claude 분류 → Notion 적재까지 한 번에 처리. 길지만 한 번만 붙여넣으면 됩니다.

Code.gs — main
const P = PropertiesService.getScriptProperties();
const NOTION = { token: P.getProperty('NOTION_TOKEN'), db: P.getProperty('NOTION_DB_ID') };
const CLAUDE = { key: P.getProperty('ANTHROPIC_API_KEY'), model: 'claude-haiku-4-5-20251001' };
const TG = { token: P.getProperty('TELEGRAM_TOKEN'), chat: P.getProperty('TELEGRAM_CHAT_ID') };

function fetchAndClassify() {
  const seen = new Set(loadRecentUrls()); // 중복 방지
  const picked = [];

  for (const src of SOURCES) {
    try {
      const xml = UrlFetchApp.fetch(src.url, { muteHttpExceptions: true }).getContentText();
      const doc = XmlService.parse(xml);
      const items = doc.getRootElement().getDescendants()
        .map(e => e.asElement && e.asElement()).filter(Boolean)
        .filter(e => e.getName() === 'item' || e.getName() === 'entry');
      for (const it of items.slice(0, 5)) {
        const title = (it.getChildText('title') || '').trim();
        const link  = it.getChildText('link') || (it.getChild('link')?.getAttribute('href')?.getValue() || '');
        if (!title || !link || seen.has(link)) continue;
        picked.push({ title, url: link, source: src.name });
      }
    } catch (e) { Logger.log('fail ' + src.name + ' ' + e); }
  }

  // Claude로 분류 + 점수 + 요약 (배치)
  const classified = classifyBatch(picked);

  for (const r of classified) addToNotion(r);

  Logger.log('fetched ' + classified.length);
}

function classifyBatch(items) {
  if (!items.length) return [];
  const prompt = `다음 기사들을 SQNC(웰니스 큐레이션) 관점에서 분류해주세요.
카테고리: 리트릿 / F&B / 공간 / 문화·체험 / 기타
score는 SQNC 독자에게 얼마나 흥미로운지 1~10.

JSON 배열만 반환:
[{"title":"...", "category":"리트릿", "score":7, "summary":"한 줄"}]

기사들:
` + items.map((x, i) => `${i+1}. ${x.title}`).join('\n');

  const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': CLAUDE.key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({
      model: CLAUDE.model,
      max_tokens: 2000,
      messages: [{ role: 'user', content: prompt }]
    }),
    muteHttpExceptions: true,
  });
  const json = JSON.parse(res.getContentText());
  const text = json.content?.[0]?.text || '[]';
  const arr  = JSON.parse(text.match(/\[[\s\S]*\]/)?.[0] || '[]');
  return items.map((it, i) => ({ ...it, ...(arr[i] || {}) }));
}

function addToNotion(r) {
  UrlFetchApp.fetch('https://api.notion.com/v1/pages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + NOTION.token, 'Notion-Version': '2022-06-28' },
    payload: JSON.stringify({
      parent: { database_id: NOTION.db },
      properties: {
        Title:    { title: [{ text: { content: r.title.slice(0, 200) } }] },
        URL:      { url: r.url },
        Source:   { select: { name: r.source } },
        Category: { select: { name: r.category || '기타' } },
        Score:    { number: r.score || 5 },
        Summary:  { rich_text: [{ text: { content: (r.summary || '').slice(0, 1900) } }] },
        Status:   { select: { name: 'candidate' } },
        FetchedAt:{ date: { start: new Date().toISOString() } },
      }
    }),
    muteHttpExceptions: true,
  });
}

function loadRecentUrls() {
  // 최근 7일 적재된 URL 가져와 중복 방지
  const res = UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${NOTION.db}/query`, {
    method: 'post',
    contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + NOTION.token, 'Notion-Version': '2022-06-28' },
    payload: JSON.stringify({ page_size: 100, sorts: [{ property: 'FetchedAt', direction: 'descending' }] }),
    muteHttpExceptions: true,
  });
  const j = JSON.parse(res.getContentText());
  return (j.results || []).map(p => p.properties?.URL?.url).filter(Boolean);
}

매일 다이제스트 푸시 — pushDailyDigest()

아침 8시에 점수 상위 7건만 묶어 한 메시지로 보냅니다.

Code.gs — daily digest
function pushDailyDigest() {
  const since = new Date(Date.now() - 24*3600*1000).toISOString();
  const res = UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${NOTION.db}/query`, {
    method: 'post', contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + NOTION.token, 'Notion-Version': '2022-06-28' },
    payload: JSON.stringify({
      filter: { and: [
        { property: 'Status',    select:   { equals: 'candidate' } },
        { property: 'FetchedAt', date:     { on_or_after: since } },
        { property: 'Score',     number:   { greater_than_or_equal_to: 6 } },
      ]},
      sorts: [{ property: 'Score', direction: 'descending' }],
      page_size: 7,
    }),
  });
  const rows = JSON.parse(res.getContentText()).results || [];
  if (!rows.length) return;

  const lines = rows.map((p, i) => {
    const t = p.properties?.Title?.title?.[0]?.plain_text || '';
    const u = p.properties?.URL?.url || '';
    const c = p.properties?.Category?.select?.name || '';
    return `${i+1}. [${c}] ${t}\n${u}`;
  });
  const text = `☀️ 오늘의 큐레이션 후보 (${rows.length})\n\n` + lines.join('\n\n');
  sendTelegram(text);
}

function sendTelegram(text) {
  UrlFetchApp.fetch(`https://api.telegram.org/bot${TG.token}/sendMessage`, {
    method: 'post',
    payload: { chat_id: TG.chat, text: text, disable_web_page_preview: 'true' },
    muteHttpExceptions: true,
  });
}

월간·분기 리포트 초안 — generateReport()

매월 1일 / 분기 첫날 자동 실행. 기간의 published 항목을 모아 Claude에게 요약·예측 초안을 부탁하고 Telegram에 전달.

Code.gs — periodic report
function generateMonthlyReport() { generateReport(30, '월간'); }
function generateQuarterlyReport() { generateReport(90, '분기'); }

function generateReport(days, label) {
  const since = new Date(Date.now() - days*24*3600*1000).toISOString();
  const res = UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${NOTION.db}/query`, {
    method: 'post', contentType: 'application/json',
    headers: { 'Authorization': 'Bearer ' + NOTION.token, 'Notion-Version': '2022-06-28' },
    payload: JSON.stringify({
      filter: { and: [
        { property: 'Status', select: { equals: 'published' } },
        { property: 'FetchedAt', date: { on_or_after: since } },
      ]},
      page_size: 100,
    }),
  });
  const rows = JSON.parse(res.getContentText()).results || [];
  const bullets = rows.map(p => `- [${p.properties?.Category?.select?.name || ''}] ${p.properties?.Title?.title?.[0]?.plain_text || ''} — ${p.properties?.Summary?.rich_text?.[0]?.plain_text || ''}`).join('\n');

  const prompt = `SQNC(한국 웰니스 큐레이션) 관점에서 지난 ${label}의 핵심 흐름을 정리하고 다음 ${label}을 예측해주세요.

지난 기간 큐레이션 목록:
${bullets}

다음 형식으로:
## 핵심 흐름 5
1. ...

## 카테고리별 변화 (리트릿/F&B/공간/문화·체험)
- ...

## 다음 ${label} 예측 3
1. ...`;

  const r = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post', contentType: 'application/json',
    headers: { 'x-api-key': CLAUDE.key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 3000, messages: [{ role:'user', content: prompt }] }),
  });
  const text = JSON.parse(r.getContentText()).content?.[0]?.text || '(생성 실패)';
  sendTelegram(`📊 ${label} 리포트 초안\n\n` + text);
}
04 Step 4 · Triggers

Step 4 — 자동 실행 트리거 (5분)

Apps Script 좌측 시계 아이콘(Triggers) → "Add Trigger" 4개 등록.

함수유형주기시간대
fetchAndClassifyTime-driven매일새벽 6시
pushDailyDigestTime-driven매일오전 8시
generateMonthlyReportTime-driven매월 1일오전 9시
generateQuarterlyReportTime-driven분기 첫날오전 9:30 (수동 토글)
분기 트리거. Apps Script는 "매월 1일"까지만 기본 옵션이라, 분기 첫날만 실행하려면 generateMonthlyReport 안에서 new Date().getMonth() % 3 === 0일 때만 분기 함수도 같이 호출하도록 한 줄 추가하세요.
05 Daily Routine

매일 운영 흐름 (15분)

06:00
자동 수집
Apps Script가 RSS 10곳에서 신규 글을 모아 Claude로 분류·점수·요약 후 Notion에 적재. 본인은 아무것도 안 함.
08:00
Telegram 다이제스트 도착
"오늘의 큐레이션 후보 7건" 메시지. 출퇴근길 이동 중 1차 훑기.
아무 때나
Notion 모바일에서 검수
Status를 candidate → published로 바꾸고 한 줄 코멘트. 이게 "발행". 5~7건 검수에 10~15분.
매월 1일
월간 리포트 초안 도착
Telegram에 마크다운 초안 도착. 그대로 둘지 / 다듬어 SQNC 사내에 공유할지 결정.
06 First Push

24~48시간 안에 첫 푸시까지

오늘 (Day 0) — 60~90분

01
계정·키 4개 발급 (30분)
Notion / Google / Telegram / Anthropic — 위 Prerequisites 그대로.
02
Notion DB 만들고 토큰 연결 (15분)
Step 1 그대로. 컬럼 타입 정확히 맞추기.
03
Apps Script 코드 붙여넣기 + Properties 4개 등록 (30분)
Step 3 코드 그대로. 저장 후 fetchAndClassify 1회 수동 실행해서 Notion에 행이 들어오는지 확인.

내일 (Day 1) — 30분

04
트리거 4개 등록 + 첫 다이제스트 발송
pushDailyDigest를 한 번 수동 실행해서 Telegram에 도착하는지 확인. 도착하면 1단계 완성.
05
RSS 소스 본인 취향대로 가감
처음 며칠 받아보고 자주 등장하는 출처는 강화, 노이즈는 제거.
06
스코어 임계값 조정
pushDailyDigest의 greater_than_or_equal_to: 6을 본인 푸시 양에 맞게 6~8 사이로 튜닝.
07 When to Upgrade

PWA로 갈 시점 — 신호 4가지

✓ 흐름이 굳었을 때
2~4주 매일 검수가 자연스러운 일과가 됨. 흐름이 본인에게 맞다는 검증 완료.
✓ 검색·아카이브가 답답할 때
Notion 모바일 검색이 느려 답답해짐 → 전용 PWA에서 카테고리·날짜 필터 + 풀텍스트 검색으로 해결.
✓ 회사 자산화 의사결정
"이걸 SQNC 멤버한테도 보여주자"라는 결정이 서면 → 다중 사용자·회원 발급 필요 → PWA + Supabase.
✓ 디자인 일관성 욕구
Notion 룩이 SQNC 브랜드와 안 맞아 답답할 때 → 현재 디자인 시스템 그대로 PWA에 이식.

그 시점에 다시 만들 산출물

08 Cost · Risks

예상 비용·리스크

0원
Notion · Apps Script · Telegram
$10
월 Claude API (예상 중간값)
60분
초기 셋업 (Day 0~1 합산)
15분
하루 검수 시간
리스크 — RSS 깨짐. 일부 매체는 RSS 피드를 비공개·축약·중단합니다. 첫 1주는 매일 Logger 확인하고, 깨지는 소스는 제거하거나 Google Alerts 이메일 → Gmail 라벨 → Apps Script로 우회.
리스크 — Claude 출력 형식 깨짐. classifyBatch는 JSON 배열을 강제하지만 가끔 깨질 수 있음. 1~2주 운영하며 실패 빈도 높으면 함수 안에 try/catch + 재시도 로직 한 단계 추가.

다음 단계 제안

실제로 Day 0 절차 진행하다가 막히는 단계가 있으면 그 단계 이름만 말씀해주세요 — 해당 단계만 줌인한 보조 가이드(스크린샷 묘사 포함)를 만들어 드립니다. 모든 단계가 잘 굴러가서 2~4주 뒤 PWA 단계로 가실 때면 같은 톤으로 PWA MVP 4주 계획서를 다시 만들어 드릴게요.