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 계산 ─────┘
사용한 도구
- n8n: 워크플로우 자동화 (Self-hosted, Docker)
- Todoist: 할 일 관리 (API 연동)
- Obsidian: 노트 관리 (Task.md에 #todo 태그)
- Discord Webhook: 알림 수신
- Discord Bot:
/todo슬래시 커맨드
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으로 전송 |
핵심 포인트
- 두 개의 트리거: Schedule(자동) + Webhook(수동) 모두 같은 파이프라인에 연결
- 병렬 처리: Obsidian과 Todoist를 동시에 조회 후 Merge 노드에서 합침
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로 우선순위 높은 작업을 가져옵니다.
- URL:
https://api.todoist.com/api/v1/tasks - Auth: Bearer Auth (Todoist API Token)
- Query:
filter=p1
주의 (2026년 기준): Todoist가 REST API v2를 폐지(
410 Gone)했습니다.
- 기존:
/rest/v2/tasks→ 현재:/api/v1/tasks- 경로의
rest를api로 바꾸는 게 핵심입니다.
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 Webhook은 Bot과 달리 멘션 처리가 다릅니다:
- embed 안의
<@ID>는 알림이 안 옵니다 — 텍스트로만 표시됨 content필드 (embed 밖)에<@ID>를 넣어야 알림이 트리거됩니다allowed_mentions필드가 반드시 있어야 합니다 — 없으면 Webhook이 멘션을 차단합니다
이 3가지를 모르면 "봇에서는 멘션이 되는데 Webhook에서는 안 된다"는 상황에 빠집니다.
Step 5: Discord 슬래시 커맨드 /todo 추가
매일 아침 자동 알림 외에, 원할 때 /todo로 수동 조회도 가능하게 했습니다.
n8n 측: Webhook 노드 추가
워크플로우에 Webhook 노드를 추가하고, 기존 Schedule Trigger와 같은 노드들에 연결합니다.
- Path: 원하는 이름 (예:
call-todo) - HTTP Method: GET
- Response: Immediately (봇 타임아웃 방지)
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/**입니다. rest → api 변경이 핵심.
❌ 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
마무리
이 시스템의 장점:
- 아침 자동 알림: 하루 시작할 때 할 일을 한눈에 파악
- 수동 조회:
/todo로 원할 때 언제든 확인 - D-day 시각화: 마감일 임박한 작업이 이모지로 바로 눈에 들어옴
- P1 우선 정렬: 긴급한 작업이 항상 상단에 표시
- @멘션 알림: 스마트폰에서 Discord 푸시 알림으로 확인
Obsidian + Todoist + n8n + Discord, 네 가지를 연결해서 GTD 워크플로우의 핵심인 "매일 리뷰"를 자동화한 사례였습니다.