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:
139
backend/app.py
139
backend/app.py
@@ -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 적립 완료!',
|
||||
|
||||
Reference in New Issue
Block a user