
안녕하세요~😆
지난번에 1일 1백준 알리미를 만들면서
어떤 기능을 더 만들어볼 수 있을지 고민하게 되었습니다.
그러다 구글 캘린더에서 일정을 가져와
미리 조언해주는 나만의 일정 비서가 있으면 좋겠다는 생각💡이 들었고,
이를 n8n과 LLM을 활용한 자동화로 구현해보게 되었습니다.

가끔 약속이나 중요한 일이 있는데 깜빡할 때가 있어서
그걸 미리 리마인드해주는 개인 비서가 있으면 좋겠다!
그리고 내가 오늘 하면 좋은 것도 조언해주면서!
이런 필요성을 가지고 만들게 된 게 오늘의 자동화입니다.
워크플로우 소개🐮🐶

Step 1. Schedule Trigger 노드

먼저 Schedule Trigger는 매일 오전 9시에 실행되게 설정을 했습니다.

지난번 글에서도 설명했었지만
Days Between Triggers를 1로 설정 == 1일마다 반복하라는 뜻이라서
매일 오전 9시에 이 트리거가 발동하게 됩니다.
Step 2. Google Calendar 노드

그 다음은 Google Calendar API에서 지금으로부터 2주까지의 일정을 가져오도록 했습니다.

{{ $now.plus({ week: 2}) }} 표현식이 지금으로부터 2주 뒤를 뜻합니다.
구글 OAuth2 API 키를 가져오는 부분이 복잡할 수 있는데 지난번 게시글에 쓰기도 했고,
n8n 공식 문서에서도 설명이 잘 나와 있어서 패스하도록 하겠습니다.
Step 3. Code 노드
원래는 Code 노드를 사용하지 않고 Google Calendar 노드에서
바로 제미나이 프롬프트에 해당 일정을 넣었는데
토큰 수가 너무 커져서 오류가 발생했습니다.
그래서 구글 캘린더에서 가져온 일정을 예쁘게 json 형식으로 추출하는 코드를 넣었습니다.
코드는 JavaScript 언어랑 Python 언어를 선택할 수 있는데
n8n은 JavaScript 기반으로 동작해서
Python은 별도로 설치⚠️해야 한다고 합니다.
그래서 JavaScript로 진행을 했어요.
아래는 제가 사용한 코드입니다.
const items = $items();
const events = items.map(i => {
const e = i.json;
const start = e.start?.dateTime ?? e.start?.date ?? null;
const end = e.end?.dateTime ?? e.end?.date ?? null;
const allDay = Boolean(e.start?.date && !e.start?.dateTime);
const desc = (e.description ?? "").replace(/\s+/g, " ").trim();
const descShort = desc.length > 300 ? desc.slice(0, 300) + "…" : desc;
return {
id: e.id ?? "",
title: e.summary ?? "(제목 없음)",
start,
end,
allDay,
location: e.location ?? "",
attendeesCount: Array.isArray(e.attendees) ? e.attendees.length : 0,
description: descShort,
};
});
return [{ json: { eventsForLLM: events } }];


원래는 왼쪽처럼 길었던 토익 일정이 오른쪽처럼 요약정리된 json 형식을 갖추게 됩니다.
Step 4. Google Gemini 노드

다음은 구글 제미나이 노드입니다.
일정을 보고 미리 어떤 걸 하라고 조언을 해주는 기능이 있으면 좋겠다고 생각해서
프롬프트를 작성한 후 step3에서 정리된 일정들을 프롬프트에 넣어주었습니다.

제미나이 API를 발급받은 후 그 API 키를 Credential에 넣어줬습니다.
모델은 gemini-2.5-flash로 했을 때
하루 1회 실행 기준으로 월 천 원 정도 수준이라
생각보다 부담 없이 Gemini를 사용할 수 있었습니다.
이마저도 생각 토큰을 사용하지 않는 gemini-2.5-flash-lite를 쓰면 절반 가까이 가격이 저렴해집니다.
너는 일정 관리사(비서)야.
아래는 사용자의 앞으로 2주간 Google Calendar 일정 목록이야.
일정을 분석해서 다음 정보를 만들어줘.
[목표]
1) 앞으로 2주 동안의 ‘핵심 일정’만 추려서 보여주기
2) 오늘(한국시간 기준) 미리 준비하면 좋은 것(TODO) 제안하기
3) 2주 전체 요약(바쁜 날/리스크/추천 루틴) 제공하기
[중요 일정 판단 기준]
- 72시간 이내 일정은 가중치 높게
- 준비가 필요한 일정
- title에 [약속]이 포함된 일정은 중요도는 medium으로, 그리고 이와 관련된 할 일은 추가하지 말 것
- 일정이 겹치거나 하루에 3개 이상 몰린 날은 위험으로 표시
[출력 규칙]
- 반드시 JSON만 출력(설명 텍스트 금지)
- highlights는 최대 5개
- todayPrep은 최대 6개, 각 task는 30분 이내의 작은 행동으로 쪼개기
- todayPrep에 '백준 1문제 풀기'는 필수로 넣을 것
[JSON 스키마]
{
"meta": { "timezone": "Asia/Seoul", "generatedAt": "ISO8601" },
"highlights": [
{ "date": "YYYY-MM-DD", "title": "string", "time": "HH:mm 또는 All-day",
"importance": "high|medium", "why": ["string"], "prep": ["string"] }
],
"todayPrep": [
{ "task": "string", "relatedEventTitle": "string", "due": "오늘|내일|이번주",
"estimateMinutes": 5 }
],
"twoWeekSummary": {
"theme": "string",
"busyDays": ["YYYY-MM-DD"],
"risks": ["string"],
"recommendations": ["string"]
}
}
[입력]
- today: {{ $now.setZone('Asia/Seoul').format('yyyy-MM-dd') }}
- events: {{ JSON.stringify($json.eventsForLLM) }}
약속과 관련된 할 일은 안 알려줘도 돼서 그 부분은 제외해달라고 했고,
- events: {{ JSON.stringify($json.eventsForLLM) }} 에서 {{ JSON.stringify($json.eventsForLLM) }} 이 부분이 아까 예쁘게 만든 json에서 일정 데이터를 가져오게 하는 부분입니다.
Step 5. Code 노드

그 다음은 다시 코드 노드인데요.
Gemini에서 생성한 json에서 데이터를 가져와
Discord에서 예쁘게 출력할 수 있도록 만들어 주는 역할을 합니다.

요랬던 json이

Discord에서 예쁘게 출력되도록 마크다운 형식으로 작성이 됩니다.
제가 사용한 JavaScript 코드는 다음과 같습니다.
// Gemini 노드 출력이 배열(items)로 올 수도, 단일 객체($json)로 올 수도 있어서 둘 다 대응
const root = Array.isArray($input.all()) ? $input.all() : [$json];
// 보통 첫 아이템이 Gemini 응답
const raw = root[0]?.json ?? root[0];
const text = raw?.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) {
throw new Error("Gemini 응답에서 candidates[0].content.parts[0].text 를 찾지 못했습니다.");
}
let data;
try {
data = JSON.parse(text);
} catch (e) {
throw new Error("Gemini 응답 text가 유효한 JSON이 아니어서 파싱에 실패했습니다.");
}
// Discord 메시지 생성 (2000자 제한 고려해서 너무 길면 잘라냄)
const lines = [];
lines.push("📅 **2주 일정 요약**");
lines.push(`🕘 생성 시각: ${data.meta?.generatedAt ?? "-"}`);
lines.push("");
lines.push("🔥 **핵심 일정**");
for (const h of (data.highlights ?? [])) {
lines.push(`• **${h.date} ${h.time}**`);
lines.push(` ${h.title} (${h.importance})`);
for (const p of (h.prep ?? [])) lines.push(` ▸ ${p}`);
lines.push("");
}
lines.push("🧠 **오늘 미리 하면 좋은 것**");
for (const t of (data.todayPrep ?? [])) {
lines.push(`• ${t.task} (${t.estimateMinutes}분)`);
}
lines.push("");
lines.push("🗓 **2주 요약**");
lines.push(`- 테마: ${data.twoWeekSummary?.theme ?? "-"}`);
const busy = data.twoWeekSummary?.busyDays ?? [];
if (busy.length) lines.push(`- 바쁜 날: ${busy.join(", ")}`);
for (const r of (data.twoWeekSummary?.risks ?? [])) lines.push(`⚠️ ${r}`);
let content = lines.join("\n");
// Discord content 2000자 제한
if (content.length > 1900) {
content = content.slice(0, 1900) + "\n…(이하 생략)";
}
return [{ json: { content } }];
Discord의 웹훅의 경우 2000자가 넘으면 오류가 나기 때문에
그 부분도 예외처리를 해줬습니다.👍
Step 6. HTTP Request 노드


이렇게 URL에 Discord 웹훅 URL을 넣어주고, Send Body를 On합니다.
그런 후에 Value에 아까 Code 코드에서 만든 {{ json.content }} 를 넣어주면 됩니다.
Discord 웹훅 URL의 경우, Discord 채널 >채널 편집 > 연동 > 웹 후크 > 웹후크 URL 복사를 하면 됩니다.
결과 화면

오전 9시에 트리거가 돌아가게 했는데 오전 9시마다 일정을 잘 알려주더라고요!
이제 일정을 까먹을 걱정은 안해도 될 것 같습니다.
무엇을 하면 좋을지도 조언을 해줘서 도움을 많이 받고 있어요!
✨ 마무리
이로써 LLM을 붙여봤는데 추후에는 MCP도 붙여보고, 프롬프트도 고도화 나갈 예정입니다.
일정을 미리 알려주고, 그에 맞는 조언을 해줬으면 좋겠다는 MVP는 성공해서 다행입니다.
앞으로 어떤 걸 더 자동화해볼 수 있을지
조금 더 고민해보려고 합니다.
읽어주셔서 감사합니다!🙇
'자동화' 카테고리의 다른 글
| [n8n] 구글 시트 + 디스코드로 1일 1백준 루틴 자동화하기 (2) | 2026.01.07 |
|---|