발행일: 2026-10-06(화) 11:44
← 목차로 돌아가기|하루 30분 n8n 시리즈 ② 실무 자동화 레시피 100
RECIPE P1

미니 프로젝트 — 나만의 아침 브리핑 봇 만들기

  • ▸챕터 1에서 배운 노드들을 조합해 완성형 워크플로우를 구축할 수 있다
  • ▸RSS 뉴스 + 날씨 + 캘린더 일정을 하나의 메시지로 합칠 수 있다
  • ▸Slack 또는 이메일로 매일 아침 자동 브리핑을 발송할 수 있다

✅사전 준비

  • □레시피 001~010 워크플로우 각각 정상 동작 확인
  • □Slack Webhook URL 또는 Gmail 자격증명 연결 완료
  • □OpenWeatherMap API 키 발급 (https://openweathermap.org/api)
  • □Google Calendar 자격증명 연결 완료
  • □수집할 RSS 피드 URL 2~3개 준비

전체 워크플로우 구성 순서

[Schedule Trigger] 매일 07:00 (Cron: 0 7 * * *)
       ↓
[HTTP Request] 날씨 API 호출
       ↓
[RSS Feed Read] 뉴스 피드 1~3개 병렬 수집
       ↓
[Google Calendar] 오늘 일정 조회
       ↓
[Code 노드] 메시지 조합
       ↓
[Slack / Gmail] 브리핑 발송

날씨 API 호출 — HTTP Request 설정

Method: GET
URL: https://api.openweathermap.org/data/2.5/weather

Query Parameters:
  q:     Seoul,KR
  appid: YOUR_API_KEY
  units: metric
  lang:  kr

Code 노드 — 브리핑 메시지 조합

const weather = $('HTTP Request').item.json;
const news    = $('RSS Feed Read').all().slice(0, 3);
const events  = $('Google Calendar').all();

const today = new Date().toLocaleDateString('ko-KR', {
  year: 'numeric', month: 'long', day: 'numeric', weekday: 'long'
});

// 날씨 섹션
const weatherText =
  `🌤 오늘 날씨: ${weather.weather[0].description}` +
  `  🌡 ${Math.round(weather.main.temp)}°C` +
  `  (체감 ${Math.round(weather.main.feels_like)}°C)`;

// 일정 섹션
const eventText = events.length
  ? events.map(e => `  • ${e.json.summary} (${e.json.start?.dateTime?.slice(11,16) ?? '종일'})`).join('\n')
  : '  • 오늘 일정 없음';

// 뉴스 섹션
const newsText = news
  .map((n, i) => `  ${i + 1}. ${n.json.title}`)
  .join('\n');

const message =
`📅 *${today} 아침 브리핑*

${weatherText}

📆 *오늘의 일정*
${eventText}

📰 *오늘의 주요 뉴스*
${newsText}
`;

return [{ json: { message } }];

Slack 노드 설정

Resource:  Message
Operation: Send
Channel:   #daily-briefing (또는 개인 DM)
Text:      {{ $json.message }}
Options:
  - Parse Mode: Markdown
💡 TIP뉴스 피드가 여러 개라면 Merge 노드(Mode: Combine, Combination Mode: Multiplex)를 사용해 병합한 뒤 Code 노드로 넘기세요. 피드별로 RSS Feed Read 노드를 각각 추가하고 모두 Merge 노드에 연결하면 됩니다.
⚠️ 주의Google Calendar API는 당일 자정(00:00) 이후부터 이벤트를 조회합니다. timeMin을 오늘 00:00, timeMax를 오늘 23:59로 설정해야 당일 일정만 나옵니다. 표현식: {{ $today.startOf('day').toISO() }}
⚡

워크플로우 JSON

아래 JSON을 복사해 n8n에 바로 임포트하세요

📥 n8n 임포트 방법
n8n 화면 우측 상단 메뉴 (⋮) → Import from JSON → 아래 JSON 전체 복사 후 붙여넣기 → Import 클릭
{
  "name": "mini_project_ch01_morning_briefing_bot",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "triggerAtHour": 8,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "id": "schedule-mp01-01",
      "name": "매일 오전 8시 트리거",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        200,
        400
      ]
    },
    {
      "parameters": {
        "resource": "event",
        "operation": "getAll",
        "calendarId": {
          "__rl": true,
          "mode": "id",
          "value": "primary"
        },
        "timeMin": "={{ $now.startOf('day').toISO() }}",
        "timeMax": "={{ $now.endOf('day').toISO() }}",
        "options": {
          "orderBy": "startTime",
          "singleEvents": true
        },
        "returnAll": true
      },
      "id": "gcal-mp01-02",
      "name": "오늘 일정 가져오기",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        420,
        200
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.NEWS_RSS_URL }}"
      },
      "id": "rss-mp01-03",
      "name": "뉴스 RSS 3건 수집",
      "type": "n8n-nodes-base.rssFeedRead",
      "typeVersion": 1.1,
      "position": [
        420,
        400
      ]
    },
    {
      "parameters": {
        "maxItems": 3
      },
      "id": "limit-mp01-04",
      "name": "뉴스 3건 제한",
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        640,
        400
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "https://api.openweathermap.org/data/2.5/weather",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "Seoul,KR"
            },
            {
              "name": "appid",
              "value": "={{ $env.OPENWEATHER_API_KEY }}"
            },
            {
              "name": "units",
              "value": "metric"
            },
            {
              "name": "lang",
              "value": "kr"
            }
          ]
        },
        "options": {}
      },
      "id": "http-mp01-05",
      "name": "날씨 API 요청",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        420,
        600
      ]
    },
    {
      "parameters": {
        "resource": "databasePage",
        "operation": "getAll",
        "databaseId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.NOTION_TASK_DB_ID }}"
        },
        "returnAll": true,
        "filterType": "manual",
        "filters": {
          "conditions": [
            {
              "key": "마감일",
              "condition": "date_on_or_before",
              "dateValue": {
                "type": "expression",
                "expressionValue": "={{ $now.plus({days: 3}).toFormat('yyyy-MM-dd') }}"
              }
            },
            {
              "key": "상태",
              "condition": "does_not_equal",
              "textValue": "완료"
            }
          ]
        },
        "options": {}
      },
      "id": "notion-mp01-06",
      "name": "마감 임박 작업 가져오기",
      "type": "n8n-nodes-base.notion",
      "typeVersion": 2.2,
      "position": [
        420,
        800
      ]
    },
    {
      "parameters": {
        "mode": "passThrough",
        "output": "all"
      },
      "id": "merge-mp01-07",
      "name": "4개 데이터 병합",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        860,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "// 각 소스에서 데이터 추출\nconst allItems = $input.all();\n\n// 오늘 날짜 헤더\nconst today = $now.setLocale('ko').toFormat('yyyy년 M월 d일 cccc');\n\n// 일정 섹션\nconst calItems = allItems.filter(i => i.json.kind === 'calendar#event' || i.json.summary !== undefined);\nlet scheduleSection = '🗓 *오늘의 일정*\\n';\nif (calItems.length === 0) {\n  scheduleSection += '등록된 일정이 없어요.\\n';\n} else {\n  calItems.forEach(item => {\n    const ev = item.json;\n    const title = ev.summary || '(제목 없음)';\n    const startRaw = ev.start?.dateTime || ev.start?.date || '';\n    const dt = new Date(startRaw);\n    const time = ev.start?.dateTime\n      ? `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}`\n      : '종일';\n    scheduleSection += `  • ${time} ${title}\\n`;\n  });\n}\n\n// 뉴스 섹션\nconst newsItems = allItems.filter(i => i.json.link !== undefined && i.json.title !== undefined);\nlet newsSection = '📰 *오늘의 주요 뉴스*\\n';\nif (newsItems.length === 0) {\n  newsSection += '수집된 뉴스가 없어요.\\n';\n} else {\n  newsItems.forEach((item, idx) => {\n    newsSection += `  ${idx+1}. <${item.json.link}|${item.json.title}>\\n`;\n  });\n}\n\n// 날씨 섹션\nconst weatherItem = allItems.find(i => i.json.weather !== undefined);\nlet weatherSection = '🌤 *오늘 날씨*\\n';\nif (weatherItem) {\n  const desc = weatherItem.json.weather?.[0]?.description || '';\n  const temp = Math.round(weatherItem.json.main?.temp ?? 0);\n  const tempMax = Math.round(weatherItem.json.main?.temp_max ?? 0);\n  weatherSection += `  서울 ${desc} / 현재 ${temp}°C / 최고 ${tempMax}°C\\n`;\n} else {\n  weatherSection += '날씨 정보를 가져오지 못했어요.\\n';\n}\n\n// Notion 마감 작업 섹션\nconst taskItems = allItems.filter(i => i.json.properties !== undefined);\nlet taskSection = '⚠️ *마감 임박 작업 (3일 이내)*\\n';\nif (taskItems.length === 0) {\n  taskSection += '임박한 마감 작업이 없어요. 👍\\n';\n} else {\n  taskItems.forEach(item => {\n    const title = item.json.properties?.이름?.title?.[0]?.plain_text\n      || item.json.properties?.Name?.title?.[0]?.plain_text\n      || '(제목 없음)';\n    const dueRaw = item.json.properties?.마감일?.date?.start || '';\n    const due = dueRaw\n      ? new Date(dueRaw).toLocaleDateString('ko-KR', { month: 'long', day: 'numeric' })\n      : '날짜 미정';\n    taskSection += `  • ${title} (${due}까지)\\n`;\n  });\n}\n\n// 최종 메시지 조합\nconst message = [\n  `📅 *${today} 모닝 브리핑*`,\n  '',\n  scheduleSection,\n  newsSection,\n  weatherSection,\n  taskSection,\n  '좋은 하루 되세요! ☀️'\n].join('\\n');\n\nreturn [{ json: { message } }];"
      },
      "id": "code-mp01-08",
      "name": "브리핑 메시지 통합 조합",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        500
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "#morning-briefing",
        "text": "={{ $json.message }}",
        "otherOptions": {}
      },
      "id": "slack-mp01-09",
      "name": "Slack 모닝 브리핑 발송",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1300,
        400
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "send",
        "toEmail": "={{ $env.MY_EMAIL }}",
        "subject": "={{ '[모닝 브리핑] ' + $now.setLocale('ko').toFormat('M월 d일 cccc') }}",
        "message": "={{ $json.message.replace(/\\*/g, '').replace(/<([^|>]+)\\|([^>]+)>/g, '$2 ($1)') }}",
        "options": {}
      },
      "id": "gmail-mp01-10",
      "name": "Gmail 모닝 브리핑 발송",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1300,
        600
      ]
    }
  ],
  "connections": {
    "매일 오전 8시 트리거": {
      "main": [
        [
          {
            "node": "오늘 일정 가져오기",
            "type": "main",
            "index": 0
          },
          {
            "node": "뉴스 RSS 3건 수집",
            "type": "main",
            "index": 0
          },
          {
            "node": "날씨 API 요청",
            "type": "main",
            "index": 0
          },
          {
            "node": "마감 임박 작업 가져오기",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "오늘 일정 가져오기": {
      "main": [
        [
          {
            "node": "4개 데이터 병합",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "뉴스 RSS 3건 수집": {
      "main": [
        [
          {
            "node": "뉴스 3건 제한",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "뉴스 3건 제한": {
      "main": [
        [
          {
            "node": "4개 데이터 병합",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "날씨 API 요청": {
      "main": [
        [
          {
            "node": "4개 데이터 병합",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "마감 임박 작업 가져오기": {
      "main": [
        [
          {
            "node": "4개 데이터 병합",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "4개 데이터 병합": {
      "main": [
        [
          {
            "node": "브리핑 메시지 통합 조합",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "브리핑 메시지 통합 조합": {
      "main": [
        [
          {
            "node": "Slack 모닝 브리핑 발송",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail 모닝 브리핑 발송",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "timezone": "Asia/Seoul"
  },
  "meta": {
    "chapter": 1,
    "type": "mini_project",
    "id": "mini_project_ch01",
    "title": "나만의 아침 브리핑 봇 만들기",
    "difficulty": "⭐⭐⭐",
    "estimatedTime": "60분",
    "linkedRecipes": [
      "003",
      "005",
      "007",
      "008"
    ],
    "apps": [
      "Google Calendar",
      "RSS Feed",
      "OpenWeatherMap API",
      "Notion",
      "Slack",
      "Gmail"
    ],
    "outcome": "매일 오전 8시, 오늘의 일정 / 주요 뉴스 3건 / 날씨 / 마감 임박 작업을 하나의 메시지로 Slack과 Gmail로 자동 수신",
    "workflowUrl": "www.koreaaitimes.com/books/n8n-vol2/recipe010p"
  }
}
← 이전 레시피☰ 목차다음 레시피 →