n8n + Discord로 매일 아침 할 일 알림 자동화하기

#📝️/✒️

n8n + Discord로 매일 아침 할 일 알림 자동화하기

Obsidian의 #todo 항목과 Todoist의 우선순위 작업을 매일 아침 Discord로 알림 받는 시스템을 만들었습니다. n8n 워크플로우와 Discord 슬래시 커맨드를 활용해서, 자동 알림은 물론 원할 때 /todo로 수동 조회도 가능합니다.

완성된 모습

Discord에서 매일 아침 7시에 이런 메시지가 옵니다:

@나
────────────────────
☀️ 오늘의 할 일
────────────────────
**좋은 아침입니다!** (2026. 03. 08. (토))

**Obsidian #todo** (5개)
1. 논문 초안 작성
2. 발표자료 검토
3. 회의록 정리
...

**Todoist P1** (8개)
🔴 D+3    | ⭐ 학회 발표자료 마감
🟡 D-2    | ⭐ 강의자료 준비
🟢 D-10   |  회식
🟢 D-14   |  결제일 확인
...

시스템 구성

[Schedule: 매일 아침 7시] ──┬── Obsidian Task.md → #todo 파싱 ──┐
                             │                                     ├── 결과 합치기 → Discord 전송
[Discord /todo 커맨드] ──────┤                                     │
                             └── Todoist P1 작업 → D-day 계산 ─────┘

사용한 도구


Step 1: n8n 워크플로우 구성

노드 구성 (총 8개)

순서 노드 역할
1a Schedule Trigger 매일 오전 7시 자동 실행
1b Webhook /todo 커맨드로 수동 실행
2 HTTP Request Obsidian Task.md 파일 읽기
3 Code #todo 항목 파싱
4 HTTP Request Todoist API에서 P1 작업 가져오기
5 Code D-day 계산 및 정렬
6 Merge 두 결과 합치기
7 Code Discord 메시지 조합
8 HTTP Request Discord Webhook으로 전송

핵심 포인트


Step 2: Obsidian #todo 파싱

Obsidian의 Task.md에서 미완료 체크박스 + #todo 태그가 있는 항목만 추출합니다.

// Task.md에서 미완료 #todo 파싱
const content = $input.first().json.data || '';
const lines = content.split('\n');
const todos = [];

for (const line of lines) {
  if (line.match(/- \[ \]/) && line.includes('#todo')) {
    const text = line
      .replace(/.*- \[ \]\s*/, '')
      .replace(/#todo\s*/g, '')
      .trim();
    if (text) todos.push(text);
  }
}

const todoLines = todos.map((t, i) =>
  `${i + 1}. ${t.substring(0, 80)}`
).join('\n');

return [{
  json: {
    todoCount: todos.length,
    todoFormatted: todoLines || '(없음)'
  }
}];

Step 3: Todoist P1 작업 + D-day 계산

Todoist API 설정

Todoist API로 우선순위 높은 작업을 가져옵니다.

주의 (2026년 기준): Todoist가 REST API v2를 폐지(410 Gone)했습니다.

D-day 계산 코드

// UTC → KST (UTC+9) 변환
const now = new Date();
const today = new Date(now.getTime() + 9 * 60 * 60 * 1000);
today.setHours(0, 0, 0, 0);

const items = $input.all();

// 새 API 응답: { results: [...], next_cursor } 구조
let tasks = [];
for (const item of items) {
  const d = item.json;
  if (d.results && Array.isArray(d.results)) {
    tasks = tasks.concat(d.results);
  } else if (d.content !== undefined) {
    tasks.push(d);
  } else if (Array.isArray(d)) {
    tasks = tasks.concat(d);
  }
}

const results = [];
for (const task of tasks) {
  const due = task.due ? new Date(task.due.date) : null;
  const isUrgent = task.priority === 4;

  if (!due) continue;           // 기한 없음 → 제외
  due.setHours(0, 0, 0, 0);
  const daysLeft = Math.ceil((due - today) / (1000 * 60 * 60 * 24));
  if (daysLeft > 30) continue;  // D-30 초과 → 제외

  let emoji = '🟢';
  let dDayStr = `D-${daysLeft}`;

  if (daysLeft < 0)       { emoji = '🔴'; dDayStr = `D+${Math.abs(daysLeft)}`; }
  else if (daysLeft === 0) { emoji = '🟠'; dDayStr = 'D-Day'; }
  else if (daysLeft <= 3)  { emoji = '🟡'; dDayStr = `D-${daysLeft}`; }

  results.push({ content: task.content, daysLeft, emoji, dDayStr, isUrgent });
}

// ⭐ P1(urgent) 먼저, 그 안에서 D-day 순
results.sort((a, b) => {
  if (a.isUrgent !== b.isUrgent) return b.isUrgent - a.isUrgent;
  return (a.daysLeft ?? 999) - (b.daysLeft ?? 999);
});

Todoist Priority 매핑 주의

Todoist API의 priority 값은 UI 표시와 반전되어 있습니다:

API priority UI 표시 의미
4 P1 (빨간색) 긴급 ⭐
3 P2 (주황색) 높음
2 P3 (파란색) 보통
1 P4 (없음) 일반

priority === 4가 실제 P1(가장 긴급)입니다. 이걸 모르면 P4(일반)를 P1로 착각하게 됩니다.


Step 4: Discord 메시지 조합 + 전송

메시지 조합 (Code 노드)

const items = $input.all();
let obsidian = { todoCount: 0, todoFormatted: '(없음)' };
let todoist = { taskCount: 0, taskFormatted: '(없음)' };

for (const item of items) {
  if (item.json.todoCount !== undefined) obsidian = item.json;
  if (item.json.taskCount !== undefined) todoist = item.json;
}

const now = new Date();
const dateStr = now.toLocaleDateString('ko-KR', {
  timeZone: 'Asia/Seoul',
  year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'short'
});

const message = [
  `**좋은 아침입니다!** (${dateStr})`,
  '',
  `**Obsidian #todo** (${obsidian.todoCount}개)`,
  obsidian.todoFormatted,
  '',
  `**Todoist P1** (${todoist.taskCount}개)`,
  todoist.taskFormatted,
].join('\n');

// content와 description을 분리 (멘션 알림을 위해)
return [{ json: { content: '<@DISCORD_USER_ID>', description: message } }];

Discord Webhook 전송 (HTTP Request 노드)

JSON Body (Using JSON 모드):

={"content": "{{ $json.content }}", "allowed_mentions": {"users": ["DISCORD_USER_ID"]}, "embeds": [{"title": "☀️ 오늘의 할 일", "description": {{ JSON.stringify($json.description) }}, "color": 3447003, "footer": {"text": "Obsidian + Todoist → n8n"}}]}

Discord Webhook 멘션 핵심 포인트

Discord WebhookBot과 달리 멘션 처리가 다릅니다:

  1. embed 안의 <@ID>는 알림이 안 옵니다 — 텍스트로만 표시됨
  2. content 필드 (embed 밖)에 <@ID>를 넣어야 알림이 트리거됩니다
  3. allowed_mentions 필드가 반드시 있어야 합니다 — 없으면 Webhook이 멘션을 차단합니다

이 3가지를 모르면 "봇에서는 멘션이 되는데 Webhook에서는 안 된다"는 상황에 빠집니다.


Step 5: Discord 슬래시 커맨드 /todo 추가

매일 아침 자동 알림 외에, 원할 때 /todo로 수동 조회도 가능하게 했습니다.

n8n 측: Webhook 노드 추가

워크플로우에 Webhook 노드를 추가하고, 기존 Schedule Trigger와 같은 노드들에 연결합니다.

Discord Bot 측: bot.js에 3곳만 추가

기존 bot.js를 통째로 교체하지 않고, 3곳만 추가합니다:

// 1. 환경변수 추가
const N8N_TODO_WEBHOOK = process.env.N8N_TODO_WEBHOOK_URL
  || 'http://YOUR_N8N_HOST:5678/webhook/call-todo';

// 2. 커맨드 등록 (commands 배열에 추가)
new SlashCommandBuilder()
    .setName('todo')
    .setDescription('오늘의 할 일 확인 (Obsidian #todo + Todoist P1)'),

// 3. 핸들러 (handleCommand 함수에 추가)
if (commandName === 'todo') {
    await interaction.deferReply();
    try {
        await axios.get(N8N_TODO_WEBHOOK);
        await interaction.editReply(
          '☀️ 할 일 알림을 전송합니다! 잠시만 기다려주세요...'
        );
    } catch (err) {
        await interaction.editReply(`❌ 할 일 조회 실패: ${err.message}`);
    }
}

봇 재빌드 후 Discord에서 /todo 입력하면 바로 할 일 알림이 옵니다.


삽질 기록 & 팁

Todoist API v2 폐지 (410 Gone)

2026년 기준으로 Todoist REST API v2가 완전히 폐지되었습니다.
에러 메시지에 "available under /api/v1/ prefixes"라고 힌트를 주는데,
/rest/v1/이 아니라 **/api/v1/**입니다. restapi 변경이 핵심.

❌ https://api.todoist.com/rest/v2/tasks  → 410 Gone
❌ https://api.todoist.com/rest/v1/tasks  → 410 Gone
✅ https://api.todoist.com/api/v1/tasks   → 정상

n8n JSON Body에서 줄바꿈 포함 문자열 처리

n8n의 HTTP Request 노드에서 Using JSON 모드를 쓸 때,
{{ JSON.stringify($json.message) }}가 줄바꿈/특수문자를 포함하면 JSON 파싱 에러가 납니다.

해결: 이전 Code 노드에서 데이터를 분리하고, JSON Body에서는 단순 문자열("{{ $json.field }}")과 JSON.stringify() (따옴표 없이)를 적절히 사용합니다.

n8n 컨테이너 UI 설치 시 타임존

Docker 컨테이너 UI(Portainer 등)로 n8n을 설치한 경우, 전체 General Settings에서 타임존을 변경할 수 없을 수 있습니다.
이 경우 워크플로우마다 개별로 Timezone을 설정해야 합니다.

워크플로우 편집 → ⚙️ Workflow Settings → Timezone → Asia/Seoul


마무리

이 시스템의 장점:

Obsidian + Todoist + n8n + Discord, 네 가지를 연결해서 GTD 워크플로우의 핵심인 "매일 리뷰"를 자동화한 사례였습니다.


Comments