회사 캘린더 비서 만들기 (feat. n8n, Gemini API)

잔디에 말 걸면 알아서 회의실 예약해주는 AI 비서, 직접 만들어봤습니다.


왜 만들었나

우리 회사는 회의실 예약을 Google Calendar로 관리한다. 문제는 예약할 때마다 캘린더 앱을 열고, 시간대를 확인하고, 이벤트를 직접 만들어야 한다는 것.

팀원들이 회의실 예약하러 캘린더 들어가는 걸 귀찮아하는 걸 보고 생각했다.

"잔디에서 그냥 말로 하면 안 되나?"

그래서 만들었다. 잔디 채팅창에 "내일 오후 2시에 대회의실 1시간 잡아줘" 라고 보내면, AI가 알아서 캘린더에 예약을 넣어주는 시스템.


전체 구조

[이미지: n8n 워크플로우 전체 화면]

사용한 스택은 단순하다.

  • n8n — 워크플로우 자동화 허브
  • Google Gemini — 자연어 해석 + 도구 호출 판단
  • Google Calendar API — 실제 예약 실행
  • 잔디(Jandi) — 사용자 인터페이스 (채팅)

흐름은 이렇다.

잔디 메시지 → n8n Webhook → AI Agent (Gemini) → Google Calendar → 잔디 응답

AI Agent가 중심이고, 상황에 따라 4가지 Calendar 도구 중 하나를 선택해서 실행한다.

도구

기능

create_conference_room_event

새 회의실 예약 생성

update_conference_room_event

기존 예약 수정

delete_conference_room_event

예약 삭제

search_conference_room

예약 현황 조회


1단계 — 잔디 Webhook 연동

잔디는 특정 채널에 메시지가 오면 외부 URL로 POST 요청을 보내는 Incoming/Outgoing Webhook을 지원한다.

Outgoing Webhook 설정 (잔디 → n8n)

잔디 팀 설정에서 Outgoing Webhook을 추가한다.

  • 채널: 봇이 응답할 채널 선택
  • 트리거 단어: 설정하지 않으면 모든 메시지에 반응 (봇 전용 채널 추천)
  • URL: n8n Webhook URL 입력

[이미지: 잔디 Outgoing Webhook 설정 화면]

잔디가 보내는 POST body 구조는 다음과 같다.

json

{
  "token": "...",
  "teamName": "tugether",
  "roomName": "회의실봇",
  "writerName": "홍길동",
  "text": "내일 오후 2시에 대회의실 1시간 잡아줘",
  "writer": "[email protected]"
}

Incoming Webhook 설정 (n8n → 잔디)

n8n에서 잔디로 응답을 보낼 때는 잔디의 Incoming Webhook URL을 사용한다.

잔디 채널 설정 → Incoming Webhook 추가 → URL 복사.

나중에 n8n에서 HTTP Request 노드로 이 URL에 POST하면 된다.

json

{
  "body": "✅ 예약 완료! 내일 오후 2~3시 대회의실을 예약했습니다."
}

2단계 — n8n Webhook 노드 설정

n8n에서 새 워크플로우를 만들고 Webhook 노드를 추가한다.

  • HTTP Method: POST
  • Path: 원하는 경로 (예: calendar-bot)
  • Response Mode: Respond to Webhook 노드로 분리

: Response Mode를 "Last Node"로 하면 AI 처리가 끝날 때까지 HTTP 연결을 물고 있어야 한다. Gemini 응답이 느릴 수 있으니 "Respond to Webhook" 노드를 별도로 두고 먼저 잔디에 응답하는 구조를 추천한다.

Webhook URL을 복사해서 잔디 Outgoing Webhook에 붙여넣으면 1단계 연결 완료.


3단계 — Google Calendar API 연동

Google OAuth2 설정

n8n에서 Google Calendar 노드를 쓰려면 OAuth2 인증이 필요하다.

  1. Google Cloud Console → API & Services → Credentials
  2. OAuth 2.0 클라이언트 ID 생성 (웹 애플리케이션)
  3. 승인된 리디렉션 URI에 n8n OAuth 콜백 URL 추가
    • 형식: https://[n8n도메인]/rest/oauth2-credential/callback
  4. 클라이언트 ID / Secret 복사

n8n → Credentials → Google Calendar OAuth2 API → 위 값 입력 후 연결.

Calendar 노드 4개 구성

AI Agent의 Tool 포트에 Google Calendar 노드 4개를 연결한다. 각 노드 설정:

create_conference_room_event

  • Operation: Create
  • Calendar: 회의실 캘린더 선택
  • 나머지 필드: AI가 채워줌 (Expression으로 설정)

update_conference_room_event

  • Operation: Update
  • Event ID: AI가 검색해서 넘겨줌

delete_conference_room_event

  • Operation: Delete

search_conference_room

  • Operation: Get All
  • 시간 범위: AI가 판단해서 설정

각 노드의 이름을 도구 이름과 동일하게 지정해야 AI Agent가 올바르게 인식한다.


4단계 — AI Agent 노드 + Gemini 설정

워크플로우의 핵심. AI Agent 노드를 추가하고 설정한다.

Chat Model 연결

AI Agent의 Chat Model 포트에 Google Gemini Chat Model 노드를 연결한다.

  • Model: gemini-2.0-flash (속도/비용 균형)
  • API Key: Google AI Studio에서 발급한 Gemini API 키

시스템 프롬프트 설계

AI Agent의 System Prompt가 전체 품질을 결정한다. 아래는 실제 사용 중인 프롬프트 구조다.

너는 00(test.ai)의 전문 회의실 예약 비서야. 
사용자의 입력({{ $json.body.data }})과 작성자 정보({{ $json.body.writer.email }})를 바탕으로 [예약/수정/삭제] 업무를 수행해.

### 1단계: 의도 파악 및 정보 추출
1. 의도 분류: 사용자의 요청이 '신규 예약', '시간/제목 수정', '예약 취소(삭제)' 중 무엇인지 판단해.
2. 시간 계산: 현재 시간({{ $now }})을 기준으로 시작/종료 시간을 ISO8601 형식으로 변환해. (타임존: Asia/Seoul)
   - 별도 언급 없으면 종료 시간은 시작 시간 1시간 뒤로 설정. '2시간 동안' 등 기간 언급 시 그에 맞게 계산.
3. 제목 생성: 제목 언급이 없으면 "{{ $json.body.writer.name }}님의 회의"로 자동 생성.
4. 회의실 ID 매핑: (언급 없으면 '10층 대회의실' 기본값)
   - 10층 대회의실/큰방: '[email protected]'
   - 10층 소회의실/작은방: '[email protected]'
   - 8층 대회의실/미팅룸: '[email protected]'
5. 참석자: 본문의 <@이메일> 멘션을 추출해 'get_member_emails'로 이메일을 확보하고 작성자({{ $json.body.writer.email }})를 포함해.

### 2단계: 기존 일정 조회 (수정/삭제/중복체크 필수)
- 모든 액션 전 'search_room_schedules' 도구를 호출해 해당 날짜의 전체 일정을 가져와.
- [예약/수정 시]: 요청한 시간대에 겹치는 일정이 있는지 확인해. (수정의 경우 본인 기존 일정은 제외)
- [수정/삭제 시]: 기존 일정 중 사용자가 말한 시간/제목과 일치하는 'Event ID'를 찾아내.

### 3단계: 도구 실행 (Action)
1. 신규 예약: 중복이 없을 때만 'create_room_event' 호출.
2. 일정 수정: 'Event ID'가 확인되고 변경 시간대에 중복이 없을 때 'update_room_event' 호출.
3. 일정 삭제: 'Event ID'가 확인되면 'delete_room_event' 호출.

### 4단계: 결과 응답 형식 (잔디 피드백)
모든 응답은 친절한 한국어로 하며 아래 형식을 지켜줘.

✅ 성공 시:
[예약 성공 / 수정 완료 / 삭제 완료] 되었습니다.
- 회의실: [회의실 이름]
- 일시: [시작시간] ~ [종료시간]
- 제목: [회의 제목]
- 참석자: [이름/이메일 리스트]

❌ 중복/실패 시:
요청하신 [회의실 이름]은 이미 아래 일정이 잡혀 있어 처리가 불가능합니다.
[해당 날짜 전체 일정 리스트]
- [시작~종료] : [제목]
(이후 "다른 시간으로 도와드릴까요?"라고 마무리)

User Message 설정

AI Agent의 User Message에는 잔디에서 받은 텍스트를 넘긴다.

{{ $json.body.text }}

잔디 Outgoing Webhook이 text 필드에 메시지를 담아서 보내기 때문에 이렇게 받는다.


5단계 — Respond to Webhook

AI Agent 처리가 끝나면 Respond to Webhook 노드로 잔디에 응답을 돌려준다.

  • Response Body: AI Agent의 출력값
  {{ $json.output }}

동시에 잔디 Incoming Webhook으로도 별도 POST를 보내면 채널에 메시지가 표시된다.


실제 동작 확인

잔디에서 예약 요청 메시지 보내는 화면

잔디에서 AI 응답이 돌아오는 화면

실제로 이런 요청들이 동작한다.

  • "내일 오후 3시에 소회의실 2시간 잡아줘" → 중복 확인 후 예약 생성
  • "이번 주 금요일 대회의실 예약 취소해줘" → 검색 후 삭제
  • "다음 주 월요일 오전에 어느 회의실 비어 있어?" → 가용 시간 조회
  • "아까 예약한 거 4시로 바꿔줘" → 이벤트 찾아서 업데이트

트러블슈팅

잔디에서 Webhook이 안 불릴 때

잔디 Outgoing Webhook은 Public URL이 필요하다. 로컬 n8n은 Cloudflare Tunnel이나 ngrok으로 외부 노출이 필요하다.

Gemini가 도구를 안 쓸 때

시스템 프롬프트에 "반드시 도구를 사용하라"는 지시를 명시적으로 추가한다. Gemini는 도구 사용 여부를 자체 판단하므로 프롬프트로 유도해야 한다.

날짜 계산이 틀릴 때

시스템 프롬프트에 {{ $now }}를 포함해서 현재 시각을 명시적으로 알려줘야 한다. 이게 없으면 Gemini가 날짜를 잘못 계산하는 경우가 생긴다.

이중 예약 문제

create 전에 반드시 search로 확인하도록 프롬프트에 강제하는 게 핵심이다. Freebusy API를 별도로 연동하면 더 정확하게 처리할 수 있다.


마치며

n8n + Gemini 조합으로 생각보다 빠르게 실용적인 AI 비서를 만들 수 있었다. 코드 한 줄 없이 노드 연결만으로 자연어 처리부터 Calendar API 호출까지 완성된다는 게 n8n의 강점이다.

비슷한 구조로 HR 문의 봇, 공지사항 자동 발송, 리포트 조회 봇 등으로 확장 가능하다. 잔디 대신 Slack이나 카카오워크를 Webhook으로 연결하면 그대로 이식된다.

전체 워크플로우 JSON은 추후 공유할 예정이다.

Posts