feat: AI 업셀링 CRM - Clawdbot Gateway 기반 맞춤 추천 시스템

키오스크 적립 시 Clawdbot Gateway(Claude Max)를 통해 구매 이력 기반
맞춤 제품 추천을 생성하고, 마이페이지 방문 시 바텀시트 팝업으로 표시.

- ai_recommendations SQLite 테이블 추가 (스키마 + 마이그레이션)
- clawdbot_client.py: Gateway WebSocket 프로토콜 v3 Python 클라이언트
- app.py: 추천 생성 + GET/POST API 엔드포인트
- my_page.html: 바텀시트 UI (슬라이드업 애니메이션, 1.5초 후 자동 표시)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin
2026-02-26 19:57:03 +09:00
parent a3ff69b67f
commit b5a99f7b3b
5 changed files with 506 additions and 1 deletions

View File

@@ -1109,7 +1109,7 @@ def my_page():
tx_dict['created_at'] = utc_to_kst_str(tx['created_at'])
transactions.append(tx_dict)
return render_template('my_page.html', user=user, transactions=transactions)
return render_template('my_page.html', user=user, transactions=transactions, user_id=user['id'])
# ============================================================================
@@ -1885,6 +1885,137 @@ def admin():
recent_tokens=recent_tokens)
# ============================================================================
# AI 업셀링 추천
# ============================================================================
def _generate_upsell_recommendation(user_id, transaction_id, sale_items, user_name):
"""키오스크 적립 후 AI 업셀링 추천 생성 (fire-and-forget)"""
from services.clawdbot_client import generate_upsell
if not sale_items:
return
# 현재 구매 품목
current_items = ', '.join(item['name'] for item in sale_items if item.get('name'))
if not current_items:
return
# 최근 구매 이력 수집
recent_products = current_items # 기본값
try:
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT ct.transaction_id
FROM claim_tokens ct
WHERE ct.claimed_by_user_id = ? AND ct.transaction_id != ?
ORDER BY ct.claimed_at DESC LIMIT 5
""", (user_id, transaction_id))
recent_tokens = cursor.fetchall()
if recent_tokens:
all_products = []
mssql_session = db_manager.get_session('PM_PRES')
for token in recent_tokens:
rows = mssql_session.execute(text("""
SELECT ISNULL(G.GoodsName, '') AS goods_name
FROM SALE_SUB S
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
WHERE S.SL_NO_order = :tid
"""), {'tid': token['transaction_id']}).fetchall()
for r in rows:
if r.goods_name:
all_products.append(r.goods_name)
if all_products:
recent_products = ', '.join(set(all_products))
except Exception as e:
logging.warning(f"[AI추천] 구매 이력 수집 실패 (현재 품목만 사용): {e}")
# Claude로 추천 생성
logging.info(f"[AI추천] 생성 시작: user={user_name}, items={current_items}")
rec = generate_upsell(user_name, current_items, recent_products)
if not rec:
logging.warning("[AI추천] 생성 실패 (AI 응답 없음)")
return
# SQLite에 저장
try:
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
expires_at = (datetime.now() + timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')
cursor.execute("""
INSERT INTO ai_recommendations
(user_id, transaction_id, recommended_product, recommendation_message,
recommendation_reason, trigger_products, ai_raw_response, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
user_id, transaction_id,
rec['product'], rec['message'], rec['reason'],
json.dumps([item['name'] for item in sale_items], ensure_ascii=False),
json.dumps(rec, ensure_ascii=False),
expires_at
))
conn.commit()
logging.info(f"[AI추천] 저장 완료: user_id={user_id}, product={rec['product']}")
except Exception as e:
logging.warning(f"[AI추천] DB 저장 실패: {e}")
@app.route('/api/recommendation/<int:user_id>')
def api_get_recommendation(user_id):
"""마이페이지용 AI 추천 조회"""
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
cursor.execute("""
SELECT id, recommended_product, recommendation_message, created_at
FROM ai_recommendations
WHERE user_id = ? AND status = 'active'
AND (expires_at IS NULL OR expires_at > ?)
ORDER BY created_at DESC LIMIT 1
""", (user_id, now))
rec = cursor.fetchone()
if not rec:
return jsonify({'success': True, 'has_recommendation': False})
# 표시 횟수 업데이트
cursor.execute("""
UPDATE ai_recommendations
SET displayed_count = displayed_count + 1,
displayed_at = COALESCE(displayed_at, ?)
WHERE id = ?
""", (now, rec['id']))
conn.commit()
return jsonify({
'success': True,
'has_recommendation': True,
'recommendation': {
'id': rec['id'],
'product': rec['recommended_product'],
'message': rec['recommendation_message']
}
})
@app.route('/api/recommendation/<int:rec_id>/dismiss', methods=['POST'])
def api_dismiss_recommendation(rec_id):
"""추천 닫기"""
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
cursor.execute("""
UPDATE ai_recommendations SET status = 'dismissed', dismissed_at = ?
WHERE id = ?
""", (now, rec_id))
conn.commit()
return jsonify({'success': True})
# ============================================================================
# 알림톡 로그
# ============================================================================
@@ -2195,6 +2326,12 @@ def api_kiosk_claim():
except Exception as alimtalk_err:
logging.warning(f"[알림톡] 발송 예외 (적립은 완료): {alimtalk_err}")
# AI 업셀링 추천 생성 (fire-and-forget)
try:
_generate_upsell_recommendation(user_id, transaction_id, sale_items, user_name)
except Exception as rec_err:
logging.warning(f"[AI추천] 생성 예외 (적립은 완료): {rec_err}")
return jsonify({
'success': True,
'message': f'{claimed_points}P 적립 완료!',