회사 캘린더 비서 만들기 (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": "ourteam",
  "roomName": "회의실봇",
  "writerName": "홍길동",
  "text": "내일 오후 2시에 대회의실 1시간 잡아줘",
  "writer": "user@company.com"
}

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층 대회의실/큰방: '~~8757c93@group.calendar.google.com'
   - 10층 소회의실/작은방: '~~b2d83@group.calendar.google.com'
   - 8층 대회의실/미팅룸: '~~549@group.calendar.google.com'
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를 보내면 채널에 메시지가 표시된다.


실제 동작 확인

잔디에서 회의실 예약을 요청하는 메시지 화면

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

요청을 받아 Google Calendar에 일정이 등록된 결과 화면

잔디에서 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