|
| 1 | +--- |
| 2 | +title: "AI로 사내 회의실 예약봇 만들기 — n8n부터 Chrome 확장까지" |
| 3 | +description: "매번 Wiki 페이지를 찾아 표를 병합하는 번거로움을 없애기 위해 만든 회의실 예약봇 개발기" |
| 4 | +date: 2026-03-28 +00:00:00 |
| 5 | +permalink: /posts/2026-03-28-ai/ |
| 6 | +mermaid: true |
| 7 | +categories: [Blogging,ai] |
| 8 | +--- |
| 9 | + |
| 10 | +## **왜 만들었는가** |
| 11 | + |
| 12 | +사내에서는 직원들이 회의실을 잡을 때 Atlassian Confluence Wiki를 사용하여 예약을 하고 있습니다. |
| 13 | + |
| 14 | + |
| 15 | + |
| 16 | +회의실을 잡을 때마다 이번 주 Wiki 페이지를 찾아야 하고, 1시간을 예약한다면 표를 병합해야 한다는 번거로움이 있었습니다. 팀 업무상 회의가 많다 보니 이러한 문제를 해결하기 위해 사내 회의실 예약봇을 AI를 통해 만들기로 하였습니다. |
| 17 | + |
| 18 | +### **주요 기능** |
| 19 | + |
| 20 | +1. **팀 전체가 사용할 수 있어야 한다** |
| 21 | + - Wiki 계정(아이디/비밀번호)으로 로그인 |
| 22 | + - 채팅 한 줄로 예약할 수 있는 간편한 UI |
| 23 | +2. **예약자명 자동 처리** |
| 24 | + - 예약 셀에 "부서명 (@사용자이름)" 형식으로 기록 |
| 25 | + - 로그인한 계정으로 Confluence `@멘션` 자동 삽입 |
| 26 | +3. **자연어 예약 / 조회** |
| 27 | + - "금요일 오후 2시 4회의실 예약해줘"처럼 말하면 됨 |
| 28 | + - 예약뿐 아니라 "비어있어?"로 가용 여부 조회 가능 |
| 29 | +4. **요일 생략 시 오늘 날짜 자동 인식** |
| 30 | + - "2시 4회의실" 입력만으로 오늘 날짜에 예약 |
| 31 | + - 주말 입력 시 다음 주 월요일로 자동 처리 |
| 32 | +5. **중복 예약 방지 및 만석 감지** |
| 33 | + - 이미 예약된 시간대에 요청 시 충돌 안내 |
| 34 | + - 해당 요일 전체 만석일 경우 별도 안내 |
| 35 | +6. **소요 시간 지정** |
| 36 | + - 기본 1시간 (2슬롯 rowspan), "30분", "2시간" 등 직접 지정 가능 |
| 37 | + |
| 38 | +### **사용한 Wiki API** |
| 39 | + |
| 40 | +| **API** | **용도** | |
| 41 | +| --- | --- | |
| 42 | +| `GET /rest/api/content` | 이번 주 예약 페이지 검색 (`title`, `spaceKey` 파라미터) | |
| 43 | +| `GET /rest/api/user?username=` | 로그인 사용자 검증 및 Confluence userKey 조회 | |
| 44 | +| `PUT /rest/api/content/{pageId}` | 예약 완료 후 페이지 HTML 업데이트 | |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## **과정** |
| 49 | + |
| 50 | +처음에는 n8n을 활용해 챗봇을 만들려 했습니다. 사내에서 n8n을 통한 Wiki 페이지 생성과 자동화 효율성이 입증되면서, 팀 전체가 n8n 도입에 열광했기 때문입니다. 자연스럽게 첫 번째 프로토타입도 n8n으로 시작되었습니다. |
| 51 | + |
| 52 | +하지만 n8n을 셀프 호스팅으로 운영하다 보니, 비개발자 팀원들이 이를 활용하려면 각자 환경을 구축하거나 복잡한 설정 과정을 거쳐야 하는 번거로움이 있었습니다. 도구가 오히려 업무의 짐이 되는 상황이었죠. |
| 53 | + |
| 54 | +고민 끝에 결정한 해답은 **크롬 확장 프로그램**이었습니다. 우리 팀의 업무 환경을 관찰해 보니, 개발자와 비개발자 할 것 없이 업무의 90% 이상이 웹 브라우저 안에서 이루어지고 있었습니다. WEHAGO 메신저, 사내 Wiki, 각종 서비스까지 모두 크롬 탭 안에 있었죠. |
| 55 | + |
| 56 | +별도의 설치나 학습 없이, 항상 켜져 있는 브라우저에서 아이콘 클릭 한 번으로 예약을 끝내는 경험. Wiki 계정만 있다면 누구나 즉시 사용할 수 있는 이 구조야말로 우리 팀의 업무 동선에 가장 자연스럽게 녹아드는 자동화라고 생각합니다. |
| 57 | + |
| 58 | +### **1. n8n 챗봇 버전 (`Wiki 회의실 예약 챗봇`)** |
| 59 | + |
| 60 | +첫 번째로 만든 건 순수 n8n 워크플로우 기반의 채팅봇입니다. n8n 내장 Chat Trigger UI로 대화하면 Gemini AI가 자연어를 파싱하고, Confluence REST API로 Wiki 페이지를 조회·수정합니다. |
| 61 | + |
| 62 | +**워크플로우 구조** |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | +``` |
| 67 | +[Chat Trigger] |
| 68 | + ↓ |
| 69 | +[Gemini - 자연어 파싱] |
| 70 | + → { room, day, time, action, meeting_name, duration_slots } |
| 71 | + ↓ |
| 72 | +[Code - 이번 주 날짜 계산] |
| 73 | + ↓ |
| 74 | +[HTTP - Wiki 페이지 검색] |
| 75 | + ↓ |
| 76 | +[IF - 페이지 존재 여부] |
| 77 | + ├─ 없음 → "이번 주 예약 페이지를 찾을 수 없습니다." |
| 78 | + └─ 있음 → |
| 79 | + [HTTP - 로그인 사용자 userKey 조회] |
| 80 | + ↓ |
| 81 | + [Code - 테이블 파싱 & 가용성 확인] |
| 82 | + ↓ |
| 83 | + [IF - book / check 분기] |
| 84 | + ├─ check → 가용 여부 메시지 반환 |
| 85 | + └─ book → |
| 86 | + [IF - 예약 가능 여부] |
| 87 | + ├─ 불가 → 충돌 안내 |
| 88 | + └─ 가능 → |
| 89 | + [Code - HTML rowspan 수정] |
| 90 | + ↓ |
| 91 | + [HTTP - Wiki PUT 업데이트] |
| 92 | + ↓ |
| 93 | + [Code - 완료 안내] |
| 94 | +``` |
| 95 | + |
| 96 | +핵심은 Wiki 테이블의 HTML을 직접 파싱하고 rowspan을 수동 계산하는 부분입니다. Confluence는 1시간 예약 시 첫 번째 셀에 `rowspan="2"`를 삽입하고 다음 행의 해당 `<td>`를 제거하는 방식으로 병합합니다. 단순히 셀을 교체하는 것이 아니라 rowspan 추적 맵을 유지하며 가상 컬럼 인덱스를 계산해야 했습니다. |
| 97 | + |
| 98 | +### **2. 크롬 확장 프로그램 버전** |
| 99 | + |
| 100 | +n8n 챗봇은 잘 동작했지만 **팀원들이 n8n을 설치해야 쓸 수 있다**는 문제가 있었습니다. 크롬 확장은 폴더 하나만 배포하면 되기 때문에 팀 전체가 설치 없이 바로 사용할 수 있습니다. |
| 101 | + |
| 102 | +### **2.1 n8n Webhook 연동 크롬 확장 (`Wiki 회의실 예약 - Webhook`)** |
| 103 | + |
| 104 | +역할을 명확히 나눈 구조입니다. **로그인과 인증은 크롬 확장이 직접** 처리하고, **자연어 파싱과 Wiki API 작업은 n8n Webhook이** 담당합니다. |
| 105 | + |
| 106 | +**동작 흐름:** |
| 107 | + |
| 108 | +``` |
| 109 | +[크롬 확장] |
| 110 | + ↓ 팝업 열림 |
| 111 | +[내부망 체크] — wiki.사내.com:8080 HEAD 요청 (5초 타임아웃) |
| 112 | + ├─ 실패 → "사내 내부망 사용자만 이용 가능합니다." |
| 113 | + └─ 성공 → |
| 114 | + [세션 확인] |
| 115 | + ├─ 세션 없음 → [로그인 화면] |
| 116 | + │ ↓ ID / PW 입력 |
| 117 | + │ Confluence API: GET /rest/api/user?username= |
| 118 | + │ → userKey, displayName 수신 |
| 119 | + │ → authHeader(Base64 인코딩)를 세션에 저장 (평문 PW 미저장) |
| 120 | + └─ 세션 있음 → [채팅 화면] |
| 121 | + ↓ 채팅 입력 |
| 122 | + n8n Webhook POST /webhook/room-booking |
| 123 | + body: { chatInput, authHeader, userKey, defaultTitle } |
| 124 | +``` |
| 125 | + |
| 126 | +로그인 시 `btoa(id:pw)`로 만든 `authHeader`만 세션에 보관하고, n8n에 전달할 때도 이 값을 그대로 사용합니다. n8n은 받은 `authHeader`로 Confluence API를 직접 호출하기 때문에 n8n 서버에 계정 정보를 따로 등록할 필요가 없습니다. |
| 127 | + |
| 128 | +**n8n이 하는 일 (Webhook 수신 후):** |
| 129 | + |
| 130 | + |
| 131 | + |
| 132 | +``` |
| 133 | +[Webhook Trigger] — chatInput, authHeader, userKey, defaultTitle 수신 |
| 134 | + ↓ |
| 135 | +[Gemini - 자연어 파싱] |
| 136 | + → { room, day, time, action, meeting_name, duration_slots } |
| 137 | + ↓ |
| 138 | +[HTTP - Wiki 페이지 검색] ← Authorization: {authHeader} |
| 139 | + ↓ |
| 140 | +[Code - 테이블 파싱 & 가용성 확인] |
| 141 | + ↓ |
| 142 | +[IF - book / check 분기] |
| 143 | + ├─ check → 가용 여부 메시지 반환 |
| 144 | + └─ book → |
| 145 | + [Code - HTML rowspan 수정] ← userKey로 @멘션 생성 |
| 146 | + ↓ |
| 147 | + [HTTP - Wiki PUT 업데이트] ← Authorization: {authHeader} |
| 148 | + ↓ |
| 149 | + [Respond to Webhook] — 완료 메시지 반환 |
| 150 | +``` |
| 151 | + |
| 152 | +**챗봇 버전 대비 개선 사항:** |
| 153 | + |
| 154 | +| **항목** | **챗봇 버전** | **Webhook 버전** | |
| 155 | +| --- | --- | --- | |
| 156 | +| 인증 | n8n에 하드코딩된 크레덴셜 | 로그인 사용자의 authHeader를 동적으로 전달 | |
| 157 | +| 멘션 | 고정 계정 | 로그인 사용자의 Confluence userKey | |
| 158 | +| 회의명 | `CoreDevCell` 고정 | 설정 화면에서 팀별 기본값 지정 가능 | |
| 159 | +| 만석 감지 | 미처리 | 해당 요일 전체 슬롯 순회 후 안내 | |
| 160 | +| n8n 설치 | 각자 설치 필요 | 팀 공용 서버 1대만 있으면 됨 | |
| 161 | + |
| 162 | +설정 화면에서 n8n 서버 URL과 기본 회의명을 지정할 수 있어, 팀 서버로 전환할 때는 URL만 바꾸면 됩니다. |
| 163 | + |
| 164 | + |
| 165 | + |
| 166 | +### **2.2 n8n 없이 Wiki API만으로 동작하는 최종 버전** |
| 167 | + |
| 168 | +Webhook 연동 버전도 잘 동작했지만 팀 서버가 없는 환경에서는 n8n을 띄워야 한다는 의존성이 남아 있었습니다. 그래서 n8n 로직 전체를 Chrome Extension JavaScript로 이식한 **완전 독립 버전**을 만들었습니다. |
| 169 | + |
| 170 | +**변경 포인트:** |
| 171 | + |
| 172 | +- **`parser.js`** — Gemini AI 대신 규칙 기반 자연어 파싱으로 교체 요일·시간·회의실·회의명·소요시간을 정규식으로 추출하며, AI 호출 없이도 대부분의 입력을 처리 |
| 173 | +- **`booking.js`** — n8n HTTP/Code 노드를 함수로 이식 `fetchPage()` → `checkAndBook()` → Confluence PUT 흐름을 직접 구현 |
| 174 | +- **`popup.js`** — UI만 담당, n8n fetch 로직 제거 |
| 175 | + |
| 176 | +**아키텍처:** |
| 177 | + |
| 178 | +``` |
| 179 | +사용자 입력 |
| 180 | + ↓ |
| 181 | +parser.js: parseIntent(text) ← 규칙 기반 파싱 |
| 182 | + ↓ intent 객체 |
| 183 | +booking.js: checkAndBook(...) |
| 184 | + ├─ Confluence GET — 이번 주 페이지 조회 |
| 185 | + ├─ HTML 파싱 — rowspan 테이블 파싱 |
| 186 | + ├─ action=check → 가용 여부 반환 |
| 187 | + └─ action=book |
| 188 | + ├─ 만석 체크 |
| 189 | + ├─ 충돌 체크 |
| 190 | + └─ HTML 수정 → Confluence PUT |
| 191 | + ↓ |
| 192 | +popup.js: appendMessage('bot', message) |
| 193 | +``` |
| 194 | + |
| 195 | +**설치:** |
| 196 | + |
| 197 | +1. `room-booking-extension-v2` 폴더를 받는다 |
| 198 | +2. Chrome 주소창에 `chrome://extensions/` 입력 |
| 199 | +3. **개발자 모드** 켜기 → **압축해제된 확장 프로그램을 로드합니다** 클릭 |
| 200 | +4. 폴더 선택 → 완료 |
| 201 | + |
| 202 | +## **마치며** |
| 203 | + |
| 204 | +처음에는 n8n의 강력한 워크플로우 기능으로 빠르게 프로토타입을 만들었고, 그 과정에서 Confluence HTML 파싱과 rowspan 처리라는 까다로운 문제를 풀었습니다. 그 로직을 검증한 뒤 Chrome Extension으로 이식하면서 팀원 누구나 설치 없이 사용할 수 있는 형태로 발전시켰습니다. |
| 205 | + |
| 206 | +AI를 활용한 것은 **설계와 구현의 속도**였습니다. Confluence Wiki HTML 구조 분석, rowspan 알고리즘 설계, Chrome Extension 보안 설계까지 AI로 프로그래밍하면서 혼자서라면 시도도 못했던 작업을 하루 이틀 만에 완성할 수 있었습니다. |
0 commit comments