# AI 업셀링 CRM — 마이페이지 맞춤 추천 시스템 ## 개요 키오스크 적립 시 고객 구매이력을 AI가 분석하여 맞춤 제품을 추천. 고객이 알림톡 → 마이페이지 접속 시 바텀시트 팝업으로 자연스럽게 표시. ## 기술 스택 - **AI 엔진**: Clawdbot Gateway (Claude Max 구독 재활용, 추가 비용 없음) - **통신**: WebSocket (`ws://127.0.0.1:18789`) — JSON-RPC 프로토콜 - **저장소**: SQLite `ai_recommendations` 테이블 - **프론트**: 바텀시트 UI (드래그 닫기 지원) ## 전체 흐름 ``` 키오스크 적립 (POST /api/kiosk/claim) │ ├─ 1. 적립 처리 (기존) ├─ 2. 알림톡 발송 (기존) └─ 3. AI 추천 생성 (fire-and-forget) │ ├─ 최근 구매 이력 수집 (SQLite + MSSQL SALE_SUB) ├─ Clawdbot Gateway → Claude 호출 ├─ 추천 결과 → ai_recommendations 저장 └─ 실패 시 무시 (추천은 부가 기능) 고객: 알림톡 버튼 클릭 → /my-page │ ├─ 1.5초 후 GET /api/recommendation/{user_id} │ ├─ 추천 있음 → 바텀시트 슬라이드업 │ ├─ 아래로 드래그 → 닫기 │ ├─ "다음에요" → dismiss │ └─ "관심있어요!" → dismiss + 기록 │ └─ 추천 없음 → 아무것도 안 뜸 ``` ## 핵심 파일 ### `backend/services/clawdbot_client.py` Clawdbot Gateway Python 클라이언트. **Gateway WebSocket 프로토콜 (v3):** 1. WS 연결 → `ws://127.0.0.1:{port}` 2. 서버 → `connect.challenge` 이벤트 (nonce 전달) 3. 클라이언트 → `connect` 요청 (token + client info) 4. 서버 → connect 응답 (ok) 5. 클라이언트 → `agent` 요청 (message + systemPrompt) 6. 서버 → `accepted` ack → 최종 응답 (`payloads[].text`) **주요 함수:** | 함수 | 설명 | |------|------| | `_load_gateway_config()` | `~/.clawdbot/clawdbot.json`에서 port, token 읽기 | | `_ask_gateway(message, ...)` | async WebSocket 통신 | | `ask_clawdbot(message, ...)` | 동기 래퍼 (Flask에서 호출) | | `generate_upsell(user_name, current_items, recent_products)` | 업셀 프롬프트 구성 + 호출 + JSON 파싱 | | `_parse_upsell_response(text)` | AI 응답에서 JSON 추출 | **Gateway 설정:** - 설정 파일: `~/.clawdbot/clawdbot.json` - Client ID: `gateway-client` (허용된 상수 중 하나) - Protocol: v3 (minProtocol=3, maxProtocol=3) ### `backend/db/mileage_schema.sql` — ai_recommendations 테이블 ```sql CREATE TABLE IF NOT EXISTS ai_recommendations ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, transaction_id VARCHAR(20), recommended_product TEXT NOT NULL, -- "고려은단 비타민C 1000" recommendation_message TEXT NOT NULL, -- 고객에게 보여줄 메시지 recommendation_reason TEXT, -- 내부용 추천 이유 trigger_products TEXT, -- JSON: 트리거된 구매 품목 ai_raw_response TEXT, -- AI 원본 응답 status VARCHAR(20) DEFAULT 'active', -- active/dismissed displayed_count INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME, -- 7일 후 만료 displayed_at DATETIME, dismissed_at DATETIME, FOREIGN KEY (user_id) REFERENCES users(id) ); ``` ### `backend/app.py` — API 엔드포인트 | 엔드포인트 | 메서드 | 설명 | |-----------|--------|------| | `/api/recommendation/` | GET | 최신 active 추천 조회 (마이페이지용) | | `/api/recommendation//dismiss` | POST | 추천 닫기 (status→dismissed) | **추천 생성 위치**: `api_kiosk_claim()` 함수 끝부분, `_generate_upsell_recommendation()` 호출 ### `backend/templates/my_page.html` — 바텀시트 UI **기능:** - 페이지 로드 1.5초 후 추천 API fetch - 💊 아이콘 + AI 메시지 + 제품명 배지 (보라색 그라디언트) - **터치 드래그 닫기**: 아래로 80px 이상 드래그하면 dismiss - 배경 탭 닫기, "다음에요"/"관심있어요!" 버튼 - 슬라이드업/다운 CSS 애니메이션 ## AI 프롬프트 **시스템 프롬프트:** ``` 당신은 동네 약국(청춘약국)의 친절한 약사입니다. 고객의 구매 이력을 보고, 자연스럽고 따뜻한 톤으로 약 하나를 추천합니다. 반드시 JSON 형식으로만 응답하세요. ``` **유저 프롬프트 구조:** ``` 고객 이름: {name} 오늘 구매한 약: {current_items} 최근 구매 이력: {recent_products} 규칙: 1. 함께 먹으면 좋은 약 1가지만 추천 (일반의약품/건강기능식품) 2. 메시지 2문장 이내, 따뜻한 톤 3. JSON: {"product": "...", "reason": "...", "message": "..."} ``` **응답 예시:** ```json { "product": "고려은단 비타민C 1000", "reason": "감기약 구매로 면역력 보충 필요", "message": "김영빈님, 감기약 드시는 동안 비타민C도 함께 챙겨드시면 회복에 도움이 돼요." } ``` ## Fallback 정책 | 상황 | 동작 | |------|------| | Gateway 꺼져있음 | 추천 생성 스킵, 로그만 남김 | | AI 응답 파싱 실패 | 저장 안 함 | | 추천 없을 때 마이페이지 방문 | 바텀시트 안 뜸 | | 7일 경과 | `expires_at` 만료, 조회 안 됨 | | dismiss 후 재방문 | 같은 추천 안 뜸 (새 적립 시 새 추천 생성) | ## 테스트 ```bash # 1. Gateway 연결 테스트 PYTHONIOENCODING=utf-8 python -c " from services.clawdbot_client import ask_clawdbot print(ask_clawdbot('안녕')) " # 2. 업셀 생성 테스트 PYTHONIOENCODING=utf-8 python -c " import json from services.clawdbot_client import generate_upsell result = generate_upsell('홍길동', '타이레놀, 챔프시럽', '비타민C, 소화제') print(json.dumps(result, ensure_ascii=False, indent=2)) " # 3. API 테스트 curl https://mile.0bin.in/api/recommendation/1 # 4. DB 확인 python -c " import sqlite3, json conn = sqlite3.connect('db/mileage.db') conn.row_factory = sqlite3.Row for r in conn.execute('SELECT * FROM ai_recommendations ORDER BY id DESC LIMIT 5'): print(json.dumps(dict(r), ensure_ascii=False)) " ```