SDH — 케이스별 전체 플로우

전화 상담 · 사용자 실행 · 아웃바운드 3개 축으로 나눠서, 각 케이스를 끝까지 따라간다. 역할 정의는 역할분업, A 구현은 A 가이드 참조.

0. 전체 오버뷰

세 케이스가 outbound 테이블을 축으로 연결된다.

CASE 1 · 전화 상담 고객 → A → B → DB 대기 등록 A B outbound → status=대기중 / 취소요청 CASE 2 · 사용자 실행 관리자 E → 네이버 실제 실행 E 네이버 outbound → status=확정/취소/실패 CASE 3 · 아웃바운드 C → 전화 / D → 문자 C D 고객 결과 통보 + 리마인더 대기 건 통보 요청 SQLite · outbound 테이블 모든 케이스를 연결하는 단일 진실 공급원 id · service · date · time · name · phone · notes · status

CASE 1전화 상담 (인바운드)

고객이 전화를 걸어온 순간부터 통화 종료까지. A와 B가 주역, 결과는 DB에 "대기" 상태로만 남는다 (확정 없음).

고객 A · 음성 I/O B · LLM/조회 네이버 (read-only) DB · outbound

1-1 예약 가능 시간 조회 (저장 없음)

"내일 오후 4시에 커트 돼요?" 수준의 단순 문의. DB 변경 없음, B가 네이버에 read-only 조회만.

고객
A · 음성 I/O
B · GeminiChat
네이버 플레이스
전화 수신
Twilio start · caller_phone 확보
greet()
"안녕하세요 살롱드달희입니다"
TTS 음성
─── 고객 발화 ───
"내일 4시 커트 돼요?"
VAD 종료 감지 · STT
chat(text, caller_phone)
check_naver_availability (Playwright)
[14:00, 16:00]
"14시, 16시 가능합니다"
TTS 음성
─── 통화 종료 ───
끊음
transcript + wav 저장
DB 변경 없음 · 단순 조회는 아무 레코드도 남기지 않는다.

1-2 신규 예약 접수 (DB에 '대기중' 저장)

여러 턴에 걸친 slot filling. B가 모든 정보를 확보한 뒤에만 DB에 저장하고, 고객에겐 "담당자가 확인 연락드리겠습니다" 안내.

고객
A
B · GeminiChat
DB · outbound
"내일 4시 커트 예약 가능해?"
check_naver → [16:00 포함]
"네, 16시 가능해요. 예약 도와드릴까요?"
TTS
1턴 — 예약 의향 확인
"네"
chat
"성함이 어떻게 되세요?"
TTS
2턴 — 이름 수집
"홍길동"
chat
"홍길동님, 010-xxxx 로 내일 16시 커트 맞으시죠?"
TTS
3턴 — 재확인 후 저장
"네 맞아요"
chat
save_booking_request(...) → INSERT
status = '대기중'
{status: saved, id: 42}
"접수됐습니다. 담당자가 확인 연락드리겠습니다."
TTS
끊음
DB (없음) 대기중 · 이후 CASE 2 에서 E가 처리

⚠ 엣지케이스

  • LLM이 save_booking_request 대신 텍스트로 "tool_code" 를 뱉는 경우 → _retry_save() 가 temperature=0으로 재시도
  • 고객이 이름을 안 주면 B는 필수 필드 부족으로 저장하지 않고 계속 물어본다 (required: customer_name)
  • 고객이 도중 취소("아냐 됐어요") → B는 저장하지 않고 종료

1-3 취소 요청 접수 (DB에 '취소요청' 저장)

고객이 날짜/시간을 모를 수 있으므로 전화번호로 역조회. 예약자 이름 대조로 본인 확인.

고객
A
B
네이버
DB
"예약 취소하고 싶은데요"
"언제 예약이셨나요?"
"몰라요, 확인해주세요"
lookup_my_bookings(caller_phone)
[5/10 14:00 커트, 예약자:홍길동]
"성함을 말씀해 주시겠어요?"
본인 확인 — 조회된 이름 vs 고객 입력
"홍길동이에요"
chat
조회 이름과 일치
"5월 10일 14시 커트 취소 접수해 드릴까요?"
최종 확인 후 저장
"네"
chat
save_cancellation_request(...) → INSERT
status = '취소요청'
"담당자 확인 후 연락드리겠습니다."
DB (없음) 취소요청

⚠ 엣지케이스

  • 이름 불일치 → "다른 이름이나 영어 이름으로 예약하셨나요?" 1회 재확인
  • 2회 불일치 → "확인이 어렵습니다. 담당자가 확인 후 연락드리겠습니다" 종료
  • 조회 결과 없음 → 이름만 받아서 date="미확인" time="미확인" 으로 접수

CASE 2사용자 실행 (관리자 판단 + 실제 실행)

E가 주역. 관리자가 대기 리스트를 보고 판단, 네이버에 실제 예약/취소를 실행한다. 결과는 CASE 3 아웃바운드로 이어진다.

E · 관리자 + admin.html DB · outbound 네이버 (Playwright 실행) C (큐 등록 대상)

2-1 대기 리스트 열람

담당자가 /admin 페이지 진입 → 대기/취소요청 목록 조회.

관리자
브라우저
FastAPI
DB
/admin 접속
GET /admin
admin.html
─── 목록 조회 ───
GET /outbound?status=대기중
SELECT * WHERE status='대기중'
rows
[{id:42, ...}]
GET /outbound/cancel_requests
[{id:43, ...}]
대기 리스트 렌더링

2-2 '대기중' 건 처리 — 예약 확정 실행

항목 클릭 → 네이버 가용성 재확인 → 담당자가 OK → 실제 예약 실행.

관리자
브라우저
FastAPI
네이버 (Playwright)
DB
① 가용성 재확인
항목 클릭
GET /outbound/42/availability
get_available_slots() · 쿠키 세션 조회
[16:00, 18:00]
{available: true, slots}
"16시 아직 가능" 표시
② 담당자 확정 → 실제 예약 실행
[확정 실행] 클릭
POST /outbound/42/confirm
make_reservation() · Playwright submit
success
UPDATE status = '확정'
{success: true, status: '확정'}
③ TODO — C 아웃바운드 요청 큐 자동 등록
상태 전이 · 대기중 확정 또는 실패

⚠ 엣지케이스

  • 가용성 재조회 결과 만석 → 담당자가 시간 변경 후 다시 상담 (CASE 3 → 고객 전화 필요)
  • 쿠키 세션 만료 → Playwright 가 nid.naver.com 으로 redirect → 앱 푸시로 재로그인 요청
  • 12시간제/24시간제 혼동 방지 → _to_24h() 로 정규화 후 비교
  • Playwright 실행 실패 → status='실패', 담당자가 수동 확인

2-3 '취소요청' 건 처리 — 취소 실행

구조는 2-2와 동일. 네이버에서 취소 실행.

관리자
브라우저
FastAPI
네이버
DB
취소요청 항목 클릭
상세 패널 표시
[취소 실행] 클릭
POST /outbound/43/cancel_confirm
cancel_reservation() · 취소 버튼 클릭
success
UPDATE status = '취소'
{status: '취소'}
TODO — C 아웃바운드 요청 큐 자동 등록
상태 전이 · 취소요청 취소 또는 취소실패

⚠ 현재 구현 gap

  • C 큐 등록이 자동화되지 않음 — confirm/cancel 성공 시 아웃바운드 요청을 자동으로 등록하는 로직이 현재 main.py 에 없다. /outbound/{id}/confirm 끝에 추가해야 함
  • 단순 취소(/outbound/{id}/cancel) 는 네이버 액션 없이 DB만 변경 — 접수 실수 철회 용도 (네이버엔 아직 예약 안 간 상태)

CASE 3아웃바운드 (결과 통보 · 리마인더)

C와 D가 주역. CASE 2에서 생성된 요청을 소비. C는 전화로, D는 문자/알림톡으로.

C · 아웃바운드 상담사 D · 메시지 알림 고객 DB · 아웃바운드 큐

3-1 결과 통보 전화 (성공 케이스)

E가 네이버에 예약 확정 후, C가 즉시 고객에게 전화해서 결과 통보 + 확인 문자 옵션.

E
DB · 아웃바운드 큐
C · Twilio outbound
고객
D · 메시지
INSERT outbound_call (type='확정통보')
─── C 폴링 · 발신 ───
C 폴링 루프
pending 1건 반환
Twilio outbound_call
수신 ack
TTS "내일 16시 커트 예약이 확정됐습니다"
"확인 문자 보내드릴까요?"
"네"
─── D에게 전송 요청 ───
메시지 전송 요청 (C → D)
템플릿 렌더 + URL 생성
SMS/알림톡 발송 (D → 고객)
UPDATE call = notified

3-2 전화 미수신 → 즉시 메시지 발송

C가 발신했으나 고객이 받지 않은 경우 → 음성사서함 대신 D에게 즉시 메시지 요청.

C
고객
D
Twilio outbound_call
(울리는 중…)
no-answer (30초 경과)
⟶ 통화 실패 감지 · 즉시 메시지로 전환
즉시 메시지 요청 (C → D)
본문 생성 "내일 16시 커트 예약 확정 · https://sdh.kr/r/abc"
SMS 발송 (D → 고객)

3-3 예약 전일/당일 자동 리마인더 (D 단독)

C 개입 없이 D 스케줄러가 자동 발송. 전화 없이 문자만.

스케줄러 (cron)
D · 메시지
DB
고객
① 전일 10시 — 하루 전 리마인더
cron trigger (매일 10시)
SELECT bookings WHERE date = today+1
[홍길동 내일 16시 커트]
템플릿 렌더: "안녕하세요 홍길동님, 내일 오후 4시 커트 예약일입니다"
SMS 발송 → 고객
② 당일 아침 — 당일 리마인더
cron trigger (예약 당일 아침)
SMS 발송: "오늘 16시 예약입니다"

⚠ 구현 노트

  • C/D 모두 현재 미구현. 구현 순서 권장: D 스케줄러(리마인더) → C (결과 통보) → 2-웨이 대화
  • SMS/알림톡 사업자 선정 필요: Twilio(국외), 알리고/CoolSMS(국내), 카카오 알림톡은 사전 템플릿 등록
  • URL 단축(/r/abc) 은 D 책임 — 추적용 파라미터 포함 가능

3-4 실패/만석 케이스 — 재협상 필요

E가 예약 확정을 시도했으나 만석 → C가 대안 제시 → 고객 응답을 다시 B 경로로 되돌림.

E
C
고객
B / DB
확정 실패 (만석)
outbound_call
"16시가 방금 만석…18시는 어떠세요?"
"그럼 18시로 할게요"
⚠ C는 직접 확정하지 않음 — B 경로로 새 대기 레코드 재등록
INSERT outbound (18시, status='대기중')
⟶ 다시 CASE 2 (E가 처리)
원칙 · C는 판단/확정 없음 — 고객 응답은 항상 B 경로를 통해 DB 대기 레코드로 재진입

정리

케이스 주역 트리거 DB 결과 다음
1. 전화 상담 A B 고객 전화 걸어옴 대기중 / 취소요청 → 2
2. 사용자 실행 E 관리자가 리스트에서 선택 확정 / 취소 / 실패 → 3
3. 아웃바운드 C D 2의 결과 + 스케줄러 notified 플래그 종결 (재협상 시 → 1/2 로)