회사 캘린더 비서 만들기 (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 인증이 필요하다.
- Google Cloud Console → API & Services → Credentials
- OAuth 2.0 클라이언트 ID 생성 (웹 애플리케이션)
- 승인된 리디렉션 URI에 n8n OAuth 콜백 URL 추가
- 형식:
https://[n8n도메인]/rest/oauth2-credential/callback
- 형식:
- 클라이언트 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를 보내면 채널에 메시지가 표시된다.
실제 동작 확인
/image-6.png)
잔디에서 예약 요청 메시지 보내는 화면
/image-7.png)
잔디에서 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은 추후 공유할 예정이다.