SDH — 역할 분업 설계

시스템을 A · B · C · D · E 5개 역할로 분리하여 각 역할의 책임·금기·인터페이스를 고정한다. 전체 아키텍처는 설계.md, 비용 분석은 분석.md, 진행 상황은 plan.md 참조.

1. 역할 요약

역할이름한 줄 설명현재 구현
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)

2. 프로세스 흐름

AB 2-1. 인바운드 흐름

[고객 전화]
     │
     ▼
┌─────────────────────────────────────────────────────────┐
│ A: 인바운드 상담사                                      │
│  - 전화 수신, 음성 스트림 수신                          │
│  - STT (상대 발화 종료 감지)                            │
│  - 재생 중 상대 목소리 감지 → 재생 중단 + 어디까지      │
│    말했는지(재생 위치)를 포함해 B에게 전달              │
│  - 전화번호를 B에게 함께 전달 (mock 허용)               │
└─────────────────────────────────────────────────────────┘
     │  (텍스트 + 재생위치 + caller_phone)
     ▼
┌─────────────────────────────────────────────────────────┐
│ B: 생각하고 행동                                        │
│  - LLM으로 intent/슬롯 파악                             │
│  - 필요 시 read-only 조회                               │
│     · 네이버 예약 가능 시간 조회                        │
│     · 발신자 예약 내역 조회                             │
│  - 응답 텍스트 생성 → A로 반환                          │
│  - 사용자 판단 필요한 액션(예약/취소)은 DB outbound에   │
│    '대기중' / '취소요청'으로 저장 후                    │
│    "담당자가 확인 연락드리겠습니다" 만 안내             │
└─────────────────────────────────────────────────────────┘
     │  (응답 텍스트)
     ▼
┌─────────────────────────────────────────────────────────┐
│ A: 응답 재생 + 통화 종료 시 기록                        │
│  - TTS 재생                                             │
│  - 통화 종료 시: 텍스트 전문 + 음성 파일 저장           │
└─────────────────────────────────────────────────────────┘

E 2-2. 처리 실행 흐름

┌─────────────────────────────────────────────────────────┐
│ E: 처리 실행자 (관리자 UI + 실제 실행 엔진)             │
│  ① 대기 리스트 화면 (outbound='대기중' / '취소요청')    │
│  ② 담당자가 항목 선택 → 실제 실행                       │
│      · '대기중'   → 네이버에 예약 확정                  │
│      · '취소요청' → 네이버에서 취소 실행                │
│  ③ 결과를 status='확정' / '취소' / '실패'로 갱신        │
│  ④ 결과 발생 → C에게 아웃바운드 요청 등록               │
└─────────────────────────────────────────────────────────┘
     │
     ▼
   (C 작업 큐)

CD 2-3. 아웃바운드 흐름

┌─────────────────────────────────────────────────────────┐
│ C: 아웃바운드 상담사                                    │
│  ① 아웃바운드 요청 큐 확인                              │
│  ② 요청 전화번호로 발신                                 │
│  ③ 요청 내용 안내                                       │
│     (예: "X월 X일 예약이 확정되었습니다")               │
│  ④ "확인 문자 보내드릴까요?" 질문                       │
│  ⑤ 동의 시 D에게 전송 대기 등록                         │
│  ⑥ 미수신 시 → D에게 즉시 메시지 발송 요청              │
└─────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────┐
│ D: 메시지 알림 (C 보조)                                 │
│  · 메시지 본문 + 확인용 URL + HTML 생성                 │
│  · 예약 전일/당일 리마인더 자동 발송                    │
│  · 전송 채널: SMS, 카카오 알림톡 등                     │
└─────────────────────────────────────────────────────────┘

3. 역할별 상세

A 인바운드 상담사

✓ 책임
들어오는 음성을 분석해 텍스트로 B에 전달. 상대 발화 종료 판별.
B 텍스트를 음성 합성·재생.
재생 중 상대 목소리 감지 → 즉시 중단 + 재생 위치(utterance_offset)를 포함해 B에 전달.
통화 종료 시 텍스트 전문과 음성 녹음 저장.
전화번호(caller_phone)를 B에게 함께 전달 (mock 허용).
✗ 금기
의미 해석·intent 분류 · DB 기록 · 외부 시스템 호출.
입력
Twilio/VoIP 음성 스트림 (mulaw 8kHz), B의 응답 텍스트
출력 → B
{ text, utterance_offset?, caller_phone }, 통화 기록(종료 시)
출력 → 고객
합성 음성 (TTS)
현재 구현
src/voice/gateway.py (WebSocket, 3초 윈도우 → STT → B → TTS)
src/voice/stt.py, src/voice/tts.py (CLOVA)
미구현: 진짜 VAD · 바지-인 · utterance_offset 전달 · 통화 종료 기록

B 생각하고 행동

✓ 책임
A의 텍스트를 LLM에 전달해 의미 파악.
read-only 행동 수행: 네이버 예약 가능 시간 조회, 발신자 예약 내역 조회 등.
자연스러운 응답 텍스트 생성 → A 반환.
사용자 판단 필요 액션은 outbound에 대기 레코드로 저장 (대기중 / 취소요청).
고객에겐 "담당자가 확인 후 연락드리겠습니다" 만 안내.
✗ 금기
네이버 예약/취소 확정. 고객에게 직접 메시지 송신. "예약 완료", "취소되었습니다" 같은 확정 표현.
입력 ← A
{ text, utterance_offset?, caller_phone }
출력 → A
응답 텍스트
출력 → DB
outbound 레코드 (대기중 / 취소요청)
현재 구현
src/voice/llm.py (GeminiChat)
Tools: check_naver_availability, lookup_my_bookings, save_booking_request, save_cancellation_request
시스템 프롬프트가 확정 표현 금지·담당자 재확인 문구를 강제.

C 아웃바운드 상담사

✓ 책임
E가 생성한 아웃바운드 요청 큐 확인.
요청 전화번호로 발신, 요청 내용 안내.
"확인 문자 보내드릴까요?" 확인 후 D에게 전송 대기 등록.
전화 미수신 시 D에게 즉시 메시지 발송 요청.
✗ 금기
스스로 판단·결정. 고객의 재협상 요청이 오면 다시 B 경로(처리 대기 생성)로 돌아가야 함.
입력
아웃바운드 요청 큐 (E가 생성)
출력
음성 통화, D에게 메시지 전송 요청
현재 구현
미구현. Twilio outbound API 래퍼 + 요청 큐 테이블 필요.

D 메시지 알림 (C 보조)

✓ 책임
아웃바운드 메시지 본문 + 확인용 URL + HTML 생성.
예약 시간 전 알림 자동 발송 ("오늘 X시 예약일입니다" 등).
발송 채널: SMS, 카카오 알림톡 등.
✗ 금기
내용에 대한 판단·결정. C 또는 E가 내용을 결정하고 D는 포맷팅·송출만.
입력
C/E의 전송 요청 (템플릿 id + 파라미터 또는 raw 본문)
출력
고객 문자 / 알림톡
현재 구현
미구현. 템플릿 엔진 + URL 생성기 + SMS/알림톡 어댑터 필요.

E 처리 실행자 (사람 + UI)

✓ 책임
처리 대기 액션 리스트 화면 제공.
담당자가 항목을 선택해 실제 실행:
  · 대기중 → 네이버에 예약 생성 → 확정 / 실패
  · 취소요청 → 네이버 취소 → 취소 / 취소실패
처리 결과 발생 시 C에게 아웃바운드 요청을 등록해 고객 통보.
✗ 금기
— (유일한 최종 결정권자)
입력
outbound 대기 레코드
출력
네이버 실제 예약/취소, C 큐에 통보 요청, 상태 갱신
현재 구현
static/admin.html — 대기 리스트 UI
GET /outbound, GET /outbound/cancel_requests
GET /outbound/{id}/availability (네이버 재조회)
POST /outbound/{id}/confirm (예약 확정 실행)
POST /outbound/{id}/cancel_confirm (취소 실행)
미구현: 처리 결과 → C 아웃바운드 요청 큐 자동 등록

4. 데이터 흐름 — outbound 상태 머신

outbound 테이블이 B · E · C 를 이어주는 단일 진실 공급원.

             B 가 생성                        E 가 실행                 C 가 소비
  ┌──────┐   ┌────────┐  confirm 성공   ┌──────┐  통보요청 생성
  │(없음) │──▶│ 대기중 │ ──────────────▶│ 확정 │ ───────────────▶ [C 큐]
  └──────┘   │        │  confirm 실패   ┌──────┐
             │        │ ──────────────▶│ 실패 │
             └────────┘                 └──────┘
                │
                │ B 가 취소요청 생성 (별도 레코드)
                ▼
             ┌────────┐  cancel_confirm 성공  ┌──────┐
             │취소요청│ ─────────────────────▶│ 취소 │ ──▶ [C 큐]
             │        │  cancel_confirm 실패  ┌──────┐
             │        │ ─────────────────────▶│취소실패│
             └────────┘                       └──────┘
status의미누가 만드는가누가 전이시키는가
대기중신규 예약 접수, 담당자 확정 대기BE
취소요청취소 접수, 담당자 확정 대기BE
확정네이버 예약 성공E (→ C 통보)
취소네이버 취소 성공E (→ C 통보)
실패네이버 예약 실패E (→ C 안내)
취소실패네이버 취소 실패E (→ C 안내)

5. 확장 시 유지해야 할 원칙

① B는 외부 쓰기 금지. 네이버/카카오/문자 등 외부 시스템 쓰기는 B의 책임이 아니다. DB에 처리 대기 레코드만 만든다.
② E만이 최종 결정자. 대기중/취소요청확정/취소 전이는 E에서만 일어난다. AI가 자동 확정하지 않는다.
③ C는 E의 결과만 통보. 고객이 재협상을 시도하면 다시 B 경로를 통해 처리 대기로 돌아간다.
④ D는 순수 변환기. 내용 결정은 C/E가, D는 포맷팅·전송만.
⑤ A는 음성 어댑터. 의미를 모르고, 텍스트·타이밍·전화번호만 전달.
이 원칙이 깨지는 방향의 요청(예: "AI가 바로 네이버에 예약하게 하자")이 들어오면 오예약 = 직접 손해라는 비즈니스 제약을 기억하고 반드시 E를 거치도록 설계한다.