교육 기관을 위한 프로덕션급 AI 챗봇 구축: 아키텍처, 교훈 및 풀스택 심층 분석
요약
인도 교육 기관 IFDA를 위해 구축한 풀스택 AI 챗봇 플랫폼의 아키텍처와 구현 과정을 다룹니다. Next.js 14, Neon PostgreSQL, OpenAI를 활용하여 리드 캡처, 상담 예약, WhatsApp 연동을 지원하는 하이브리드 시스템을 구축했습니다.
핵심 포인트
- 비용과 속도 최적화를 위한 스크립트 퍼널과 LLM 폴백의 하이브리드 구조 채택
- Next.js 14 App Router 기반의 모노레포 아키텍처 설계
- WhatsApp API와 연동하여 실시간 고객 대응 및 리드 관리 자동화
- Prisma ORM과 Neon PostgreSQL을 사용한 안정적인 데이터 계층 구축
요약(TL;DR): 저는 인도 교육 기관인 IFDA를 위해 풀스택 AI 챗봇 플랫폼을 구축했습니다. 이 플랫폼은 강의 탐색, 리드 캡처(Lead capture), 상담 예약, WhatsApp 메시징을 처리하며, Neon PostgreSQL 백엔드와 함께 Vercel에 배포된 완전한 관리자 CRM을 포함합니다. 제가 배운 모든 것, 구축한 모든 시스템, 그리고 제가 왜 그러한 선택을 했는지에 대해 설명하겠습니다.
이 프로젝트가 존재하는 이유
교육 기관은 인간 입학 상담사에게 막대한 자원을 소비합니다. 잠재 고객들은 반복적으로 같은 질문을 합니다: 어떤 강의를 제공하나요? 프로그램 기간은 얼마나 되나요? 수료 후에는 무엇을 벌 수 있나요? 그리고 밤 11시에 답변되지 않은 모든 문의는 놓쳐버린 리드(Lead)가 됩니다. IFDA에는 정적인 FAQ 페이지보다 똑똑하고, 24시간 콜센터보다 저렴한 무언가가 필요했습니다.
해결책: 강의에 대해 지능적인 대화를 나누고, 리드를 캡처하며, 상담 예약을 잡고, 뜨거운 잠재 고객(Hot prospects)을 상담 직원에게 전달하는 동시에 WhatsApp과 동기화할 수 있는 하이브리드 AI 챗봇입니다. 이것은 제가 이를 어떻게 구축했는지에 대한 전체 기술적 이야기입니다.
아키텍처 개요
이 프로젝트는 Next.js 14 App Router 모노레포(Monorepo)로, 하나의 코드베이스가 사용자용 챗봇 위젯, 관리자 CRM 대시보드 및 모든 API 경로를 제공합니다. 모든 구성 요소는 의도적으로 선택되었습니다:
| 계층 (Layer) | 기술 (Technology) |
|---|---|
| 프레임워크 (Framework) | Next.js 14 (App Router) |
| 언어 (Language) | 전반적으로 TypeScript 사용 |
| 데이터베이스 (Database) | Neon PostgreSQL + Prisma ORM |
| AI / LLM | 의도 파악, 임베딩(Embeddings) 및 응답 생성을 위한 OpenAI (GPT) |
| 배포 (Deployment) | Vercel (Edge 호환) |
| DoubleTick API | |
| 인증 (Auth) | 커스텀 JWT + RBAC 미들웨어 |
| 세션 (Session) | 서버 사이드 세션 저장소 |
이 코드베이스의 그래프는 55개 이상의 커뮤니티 클러스터에 걸쳐 분산된 1,259개의 노드(파일, 함수, 타입)와 3,276개의 엣지(호출, 포함, 참조)를 가지고 있습니다. 이는 봇 엔진 코어부터 Prisma 엣지 런타임, 관리자 대시보드 페이지에 이르기까지 다양합니다. 각 주요 시스템을 살펴보겠습니다.
시스템 1: 봇 엔진 ( lib/bot/engine.ts )
프로젝트의 핵심은 lib/bot/engine.ts에 있는 processMessage()입니다.
이 단일 함수는 웹 위젯(web widget)이나 WhatsApp을 통해 들어오는 모든 메시지의 트래픽 컨트롤러(traffic controller) 역할을 합니다.
하이브리드 스크립트 + LLM 아키텍처 (Hybrid Scripted + LLM Architecture)
봇은 모든 메시지를 GPT로 보내지 않습니다. 그렇게 하면 속도가 느려지고 비용이 많이 들 것입니다. 대신, 다음과 같은 2단계 접근 방식(two-tier approach)을 사용합니다:
- 스크립트 퍼널 단계 (Scripted funnel stages) — 리드 캡처(이름, 전화번호, 관심 과정, 도시)를 위한 구조화된 흐름으로, 결정론적 로직(deterministic logic)이 LLM보다 더 빠르고 신뢰할 수 있습니다.
- LLM 폴백 (LLM fallback) — 개방형 질문, 과정 비교, 진로 문의 또는 정의된 단계 이외의 모든 사항을 처리합니다.
단계 관리(stage management)는 lib/ai/intent.ts에 구현되어 있습니다:
// intent.ts — 단순화된 버전
export function resolveStage ( session : Session ): FunnelStage { ... }
export function getMissingFields ( lead : Partial < Lead > ): LeadField [] { ... }
export function getNextQuestion ( missing : LeadField []): string { ... }
export function isLeadComplete ( lead : Partial < Lead > ): boolean { ... }
export function detectIntent ( message : string ): Intent { ... }
export function analyzeMessage ( message : string , session : Session ): Analysis { ... }
analyzeMessage()는 결정 함수(decision function)입니다. 이 함수는 현재 세션 단계의 문맥(context) 내에서 메시지를 평가하고, 스크립트 경로를 호출할지, lib/ai/llm.ts의 getAIResponse()를 호출할지, 또는 캐러셀(carousel)/퀵 리플라이(quick-reply) 블록을 트리거할지를 결정합니다.
구조화된 메시지 구성 요소 (Structured Message Components)
응답은 단순한 문자열이 아닙니다. 응답은 타입이 지정된 메시지 블록(typed message blocks)이며, 렌더러 레이어(renderer layer)가 이를 채널별 특정 형식으로 변환합니다:
// lib/ai/llm.ts — 블록 빌더 (block builders)
textBlock ( content : string ): TextBlock
domainListBlock ( domains : Domain []): DomainListBlock
carouselBlock ( items : CourseCarouselItem []): CarouselBlock
courseDetailBlock ( course : Course ): CourseDetailBlock
quickRepliesBlock ( replies : string []): QuickRepliesBlock
visitWebsiteBlock ( url : string ): VisitWebsiteBlock
이러한 블록 기반 설계 덕분에 동일한 엔진 출력이 웹 UI와 WhatsApp이라는 매우 다른 두 가지 렌더링 표면(rendering surfaces)을 모두 구동할 수 있습니다.
System 2: 이중 채널 렌더링 (Dual-Channel Rendering) 렌더러 계층 (renderer layer)은 구조화된 메시지 블록을 채널별 형식으로 변환합니다. Web Renderer ( lib/bot/renderers/web.ts )의 renderToWeb()은 블록을 프론트엔드의 StructureMessage.tsx 컴포넌트가 소비할 수 있는 React 호환 JSON으로 변환합니다. 이 컴포넌트는 스크롤 동작, checkScroll()을 이용한 캐러셀 (carousels), 그리고 애니메이션이 적용된 메시지 출현을 처리합니다. WhatsApp Renderer ( lib/bot/renderers/whatsapp.ts )의 renderToWhatsApp()은 동일한 블록을 DoubleTick API의 메시지 형식으로 매핑합니다. WhatsApp은 엄격한 메시지 유형 규칙을 가지고 있습니다. 대화형 리스트 (interactive lists), 템플릿 (templates), 일반 텍스트 (plain text)는 서로 다른 스키마를 가진 별도의 API 호출입니다. 렌더러는 lib/whatsapp/doubletick.ts 클라이언트를 통해 이 모든 것을 처리합니다: // doubletick.ts export async function sendWhatsAppText ( phone : string , text : string ): Promise < void > export async function sendWhatsAppTemplate ( phone : string , template : TemplateParams ): Promise < void > export async function sendWhatsAppInteractiveList ( phone : string , list : InteractiveList ): Promise < void > 수신된 WhatsApp 메시지는 app/api/whatsapp/webhook/route.ts에 도달하며, 여기서 isValidSignature()를 통해 HMAC 서명을 검증하고, lib/whatsapp/messageParser.ts를 통해 페이로드 (payload)를 파싱한 후, 동일한 processMessage() 봇 엔진으로 전달합니다. 하나의 엔진, 두 개의 채널입니다. System 3: 코스 인텔리전스 (Course Intelligence) ( lib/ai/courseData.ts ) 코스 카탈로그 (course catalog)는 시스템에서 가장 풍부한 데이터 소스입니다.
courseData.ts는 봇 엔진이 문맥적으로 정확한 응답을 생성하기 위해 호출하는 25개 이상의 내보내기(exported) 함수를 포함하는 포괄적인 모듈입니다:
- findCourse ( query : string ): Course | null
- getCourseAbout ( course : Course ): string
- getCoursePrerequisites ( course : Course ): string
- getCourseSyllabus ( course : Course ): string
- getCourseCareerOpportunities ( course : Course ): string
- getCourseCareerGrowthRoadmap ( course : Course ): string
- getCourseProfessionalGrowthLadder ( course : Course ): string
- getCourseTools ( course : Course ): string
- getCourseSkills ( course : Course ): string
- getAllCourseCarouselItems (): CourseCarouselItem []
- getCarouselByIntent ( intent : Intent ): CourseCarouselItem []
- getLLMCourseMap (): LLMCourseMap // GPT 컨텍스트 주입을 위한 압축된 맵
- getCourseContext ( course : Course ): string // LLM 프롬프트를 위한 전체 컨텍스트 문자열
findCourse()는 애플리케이션 전체에서 가장 많이 호출되는 함수(호출 그래프에서 32개의 엣지)로,
지식 파싱 파이프라인(Parsing Pipeline) knowledge-parser.ts는 다중 형식의 데이터를 수집합니다: parseKnowledgeFile ( 파일 : File ): Promise < ParsedKnowledge > parsePdf ( 버퍼 : Buffer ): Promise < string > parseCsv ( 내용 : string ): ParsedRow [] parseJson ( 내용 : string ): ParsedKnowledge parseTxt ( 내용 : string ): ParsedKnowledge chunkText ( 텍스트 : string , chunkSize : number ): string [] 시맨틱 검색(Semantic Search) lib/ai/embeddings.ts는 벡터 계층을 처리합니다: generateEmbedding ( 텍스트 : string ): Promise < number [] > // OpenAI text-embedding-3-small embedAllFaqs (): Promise < void > // 가져오기 시 일괄 임베딩(Bulk embed on import) embedNewFaqs (): Promise < void > // 증분 업데이트(Incremental update) searchSimilarFaqs ( 쿼리 : string , topK : number ): Promise < FAQ [] > 사용자가 스크립트 엔진이 다루지 않는 질문을 할 경우, getRelevantKnowledge()가 임베딩된 지식 기반에 대해 벡터 유사성 검색(vector similarity search)을 실행합니다. 상위 결과는 LLM 컨텍스트에 주입되어, 관리자가 업로드한 문서를 기반으로 정확하고 최신 답변을 제공하며 — 재배포가 필요하지 않습니다. 시스템 5: 리드 확보 및 CRM (Lead Capture & CRM)
리드 확보(Lead capture) 기능은 대화 흐름 깊숙이 통합됩니다. 봇이 여러 차례에 걸쳐 정보를 수집함에 따라, 점진적으로 리드 프로필을 구축합니다. intent.ts의 extractLeadInfo()는 자연어에서 구조화된 데이터를 추출합니다:
// 사용자 발언: "저는 Lucknow 출신의 Rohan이고 UI/UX 과정에 관심이 있습니다"
// extractLeadInfo() 반환값: { name : "Rohan" , city : "Lucknow" , courseInterest : "UI/UX Design" }
verifyHumanName()은 데이터베이스에 도달하기 전에 비현실적인 이름 문자열을 거부하여 봇 오용(bot-abuse)을 방지합니다. isLeadComplete()가 true를 반환하면, pushLeadToCRM()이 실행됩니다:
// lib/crm/leads.ts export async function pushLeadToCRM ( lead : CompleteLead ): Promise < void >
이는 Prisma를 통해 Neon PostgreSQL 데이터베이스에 기록하고, 선택적으로 리드의 번호로 WhatsApp 확인 템플릿을 트리거합니다.
관리자 측면에서는 다음을 통해 전체 CRUD를 제공합니다:
GET/POST /api/admin/leads — 리드 목록 조회 및 필터링
GET/PATCH/DELETE /api/admin/leads/[id] — 개별 리드 관리
app/admin/page.tsx에 있는 AdminLeadsPage는 단계 추적 기능이 포함된 CRM 인터페이스를 제공합니다. 상담사는 입학 퍼널 (admissions funnel)을 따라 리드를 수동으로 이동시킬 수 있으며, updateLeadStage()가 변경 사항을 영구적으로 저장합니다.
시스템 6: 예약 스케줄링 (Appointment Scheduling)
lib/scheduler/calendar.ts가 예약 흐름을 처리합니다:
getAvailableDates (): Promise < AvailableDate [] >
isSlotAvailable ( date : string , time : string ): Promise < boolean >
bookAppointment ( lead : Lead , slot : TimeSlot ): Promise < Appointment >
의도 탐지 (intent detection)가 스케줄링 의도를 식별하면 (챗봇 라우트의 handleScheduling()), 봇은 사용 가능한 시간대를 퀵 리플라이 (quick-reply) 캐러셀 형태로 제시합니다. 사용자가 하나를 선택하면, bookAppointment()가 데이터베이스에 예약을 기록하고 WhatsApp 확인 메시지를 전송합니다. app/api/schedule/route.ts는 공개 엔드포인트이며, app/api/admin/appointments/route.ts는 관리자에게 모든 예정된 예약 목록을 제공합니다.
시스템 7: 세션 관리 (Session Management)
봇은 여러 HTTP 요청에 걸쳐 상태를 유지 (stateful)합니다. lib/session/store.ts가 이를 관리합니다:
getSession ( sessionId : string ): Promise < Session | null >
saveSession ( sessionId : string , session : Session ): Promise < void >
clearSession ( sessionId : string ): Promise < void >
클라이언트 측에서는 app/chatbot/hooks/useChat.ts가 sessionId의 생명주기를 관리합니다:
export function useChat () {
// sessionId를 생성하고 유지합니다
// 메시지 기록을 관리합니다
// 로딩/에러 상태를 처리합니다
const { sessionId } = getSessionId ()
// ...
}
세션에는 퍼널 단계, 부분적인 리드 데이터, 대화 기록, 그리고 마지막으로 탐지된 의도가 포함됩니다. 이는 processMessage()가 대화가 중단된 지점부터 정확히 다시 시작하는 데 필요한 모든 정보입니다.
System 8: 관리자 대시보드 및 RBAC (Role-Based Access Control, 역할 기반 액세스 제어) 관리자 패널은 다음과 같은 여러 페이지를 포함하는 완전한 내부 애플리케이션입니다:
/admin/login: JWT 인증/admin/dashboard: 분석 차트 (BarChart), 지표 개요/admin/dashboard/members: 팀원 관리 (추가, 수정, 삭제)/admin: 단계 관리가 포함된 리드 (Lead) CRM/admin/conversations: 전체 채팅 기록 뷰어/admin/knowledge: 지식 파일 업로드/관리/admin/templates: AI 생성이 포함된 WhatsApp 템플릿 빌더/admin/profile: 프로필 관리
RBAC 구현
권한 시스템은 역할-권한 매트릭스 (role-permission matrix) 아키텍처를 사용하며, 이는 implementation_plan.md에 문서화되어 있고 lib/auth/permissions.ts에 구현되어 있습니다:
export function hasPermission ( role : AdminRole , permission : Permission ): boolean
export function getDefaultPage ( role : AdminRole ): string
미들웨어 체인 (middleware chain)은 API 레벨에서 이를 강제합니다:
// lib/auth/middleware.ts
requireAdmin () // JWT를 검증하고 인증되지 않은 요청을 거부함
requireRole ( role : AdminRole ) // 역할 기반 액세스를 강제함
getAdminFromRequest () // 토큰에서 관리자 컨텍스트를 추출함
lib/auth/jwt.ts는 토큰 생명주기를 처리합니다:
signToken ( payload : JWTPayload ): string
verifyToken ( token : string ): JWTPayload | null
verifyTokenAsync ( token : string ): Promise < JWTPayload >
관리자 UI의 사이드바는 hasPermission()을 사용하여 탐색 항목을 필터링합니다. 상담사(counselors)는 리드와 대화 내용을 볼 수 있으며, 슈퍼 관리자(superadmins)는 멤버 관리 및 분석을 포함한 모든 내용을 볼 수 있습니다.
WhatsApp 템플릿 빌더
/admin/templates 페이지는 특별히 언급할 가치가 있습니다. 이는 관리자가 여러 슬라이드로 구성된 WhatsApp 브로드캐스트 캠페인을 작성할 수 있는 AI 기반 에디터입니다:
generateWithAI (): GPT가 프롬프트로부터 슬라이드 콘텐츠를 생성함addSlide () / removeSlide (): 캠페인 구조 관리buildWhatsAppText (): 슬라이드를 WhatsApp 텍스트 형식으로 컴파일함buildHTMLBlock (): 컴파일
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기