시스템을 A · B · C · D · E 5개 역할로 분리하여 각 역할의 책임·금기·인터페이스를 고정한다. 전체 아키텍처는 설계.md, 비용 분석은 분석.md, 진행 상황은 plan.md 참조.
| 역할 | 이름 | 한 줄 설명 | 현재 구현 |
|---|---|---|---|
| A | 인바운드 상담사 귀 / 입 |
전화 음성 I/O 전담. 의미는 모른다. | src/voice/gateway.py, stt.py, tts.py |
| B | 생각·행동자 뇌 |
의미 파악 + 데이터 조회 + 응답 생성. 쓰기 금지. | src/voice/llm.py (GeminiChat) |
| C | 아웃바운드 상담사 | 결과를 전화로 통보. 판단 금지. | 미구현 |
| D | 메시지 알림 C 보조 |
문자/알림 본문·URL·HTML 생성 및 발송. | 미구현 |
| E | 처리 실행자 사람 + UI |
대기 액션 검토 후 실제 실행. 유일한 최종 결정자. | static/admin.html, /outbound/* |
┌─ 듣고 말한다 ─┐ ┌─ 생각하고 조회 ─┐ ┌─ 사람이 결정·실행 ─┐ ┌─ 알린다 ─┐
│ A │ │ B │ │ E │ │ C / D │
└──────────────┘ └─────────────────┘ └────────────────────┘ └──────────┘
음성만 의미·조회만 판단·확정·실행 통보·문자
──────── ─────────── ────────────── ──────
(의미 해석 X) (확정 액션 X) (AI 개입 X) (판단 X)
[고객 전화]
│
▼
┌─────────────────────────────────────────────────────────┐
│ A: 인바운드 상담사 │
│ - 전화 수신, 음성 스트림 수신 │
│ - STT (상대 발화 종료 감지) │
│ - 재생 중 상대 목소리 감지 → 재생 중단 + 어디까지 │
│ 말했는지(재생 위치)를 포함해 B에게 전달 │
│ - 전화번호를 B에게 함께 전달 (mock 허용) │
└─────────────────────────────────────────────────────────┘
│ (텍스트 + 재생위치 + caller_phone)
▼
┌─────────────────────────────────────────────────────────┐
│ B: 생각하고 행동 │
│ - LLM으로 intent/슬롯 파악 │
│ - 필요 시 read-only 조회 │
│ · 네이버 예약 가능 시간 조회 │
│ · 발신자 예약 내역 조회 │
│ - 응답 텍스트 생성 → A로 반환 │
│ - 사용자 판단 필요한 액션(예약/취소)은 DB outbound에 │
│ '대기중' / '취소요청'으로 저장 후 │
│ "담당자가 확인 연락드리겠습니다" 만 안내 │
└─────────────────────────────────────────────────────────┘
│ (응답 텍스트)
▼
┌─────────────────────────────────────────────────────────┐
│ A: 응답 재생 + 통화 종료 시 기록 │
│ - TTS 재생 │
│ - 통화 종료 시: 텍스트 전문 + 음성 파일 저장 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ E: 처리 실행자 (관리자 UI + 실제 실행 엔진) │
│ ① 대기 리스트 화면 (outbound='대기중' / '취소요청') │
│ ② 담당자가 항목 선택 → 실제 실행 │
│ · '대기중' → 네이버에 예약 확정 │
│ · '취소요청' → 네이버에서 취소 실행 │
│ ③ 결과를 status='확정' / '취소' / '실패'로 갱신 │
│ ④ 결과 발생 → C에게 아웃바운드 요청 등록 │
└─────────────────────────────────────────────────────────┘
│
▼
(C 작업 큐)
┌─────────────────────────────────────────────────────────┐
│ C: 아웃바운드 상담사 │
│ ① 아웃바운드 요청 큐 확인 │
│ ② 요청 전화번호로 발신 │
│ ③ 요청 내용 안내 │
│ (예: "X월 X일 예약이 확정되었습니다") │
│ ④ "확인 문자 보내드릴까요?" 질문 │
│ ⑤ 동의 시 D에게 전송 대기 등록 │
│ ⑥ 미수신 시 → D에게 즉시 메시지 발송 요청 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ D: 메시지 알림 (C 보조) │
│ · 메시지 본문 + 확인용 URL + HTML 생성 │
│ · 예약 전일/당일 리마인더 자동 발송 │
│ · 전송 채널: SMS, 카카오 알림톡 등 │
└─────────────────────────────────────────────────────────┘
{ text, utterance_offset?, caller_phone }, 통화 기록(종료 시)src/voice/gateway.py (WebSocket, 3초 윈도우 → STT → B → TTS)src/voice/stt.py, src/voice/tts.py (CLOVA)outbound에 대기 레코드로 저장 (대기중 / 취소요청).{ text, utterance_offset?, caller_phone }outbound 레코드 (대기중 / 취소요청)src/voice/llm.py (GeminiChat)check_naver_availability, lookup_my_bookings,
save_booking_request, save_cancellation_request대기중 → 네이버에 예약 생성 → 확정 / 실패취소요청 → 네이버 취소 → 취소 / 취소실패outbound 대기 레코드static/admin.html — 대기 리스트 UIGET /outbound, GET /outbound/cancel_requestsGET /outbound/{id}/availability (네이버 재조회)POST /outbound/{id}/confirm (예약 확정 실행)POST /outbound/{id}/cancel_confirm (취소 실행)outbound 상태 머신outbound 테이블이 B · E · C 를 이어주는 단일 진실 공급원.
B 가 생성 E 가 실행 C 가 소비
┌──────┐ ┌────────┐ confirm 성공 ┌──────┐ 통보요청 생성
│(없음) │──▶│ 대기중 │ ──────────────▶│ 확정 │ ───────────────▶ [C 큐]
└──────┘ │ │ confirm 실패 ┌──────┐
│ │ ──────────────▶│ 실패 │
└────────┘ └──────┘
│
│ B 가 취소요청 생성 (별도 레코드)
▼
┌────────┐ cancel_confirm 성공 ┌──────┐
│취소요청│ ─────────────────────▶│ 취소 │ ──▶ [C 큐]
│ │ cancel_confirm 실패 ┌──────┐
│ │ ─────────────────────▶│취소실패│
└────────┘ └──────┘
| status | 의미 | 누가 만드는가 | 누가 전이시키는가 |
|---|---|---|---|
대기중 | 신규 예약 접수, 담당자 확정 대기 | B | E |
취소요청 | 취소 접수, 담당자 확정 대기 | B | E |
확정 | 네이버 예약 성공 | — | E (→ C 통보) |
취소 | 네이버 취소 성공 | — | E (→ C 통보) |
실패 | 네이버 예약 실패 | — | E (→ C 안내) |
취소실패 | 네이버 취소 실패 | — | E (→ C 안내) |
대기중/취소요청 → 확정/취소 전이는 E에서만 일어난다. AI가 자동 확정하지 않는다.