- /admin/products: 전체 제품 검색 페이지 (OTC) - /api/products: 제품 검색 API (세트상품 바코드 포함) - qr_printer.py: Brother QL-710W 프린터 연동 - /api/qr-print, /api/qr-preview: QR 라벨 인쇄/미리보기 API - 판매상세 페이지에 QR 인쇄 버튼 추가 - 수량 선택 UI (+/- 버튼, 최대 10장) - 세트상품 제조사 표시 개선 - 대시보드 헤더에 제품검색/판매조회 탭 추가
325 lines
13 KiB
Markdown
325 lines
13 KiB
Markdown
# AI 업셀링 시스템 아키텍처
|
|
|
|
> 청춘약국 AI 기반 맞춤 제품 추천 시스템의 전체 구조 및 데이터 흐름
|
|
|
|
## 개요
|
|
|
|
고객이 마일리지를 적립할 때, 실시간으로 AI가 추가 구매 추천을 생성하는 시스템.
|
|
|
|
**핵심 특징:**
|
|
- POS(PIT3000) 판매 데이터 기반 추천
|
|
- 고객별 구매 이력 분석
|
|
- 약국 실제 재고(최근 판매 제품) 기반
|
|
- Clawdbot Gateway를 통한 Claude 연동 (추가 API 비용 없음)
|
|
|
|
---
|
|
|
|
## 아키텍처 다이어그램
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 전체 흐름 │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ [POS 판매] │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ [MSSQL: PM_PRES] ←─── PIT3000 POS 데이터 │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ [키오스크 적립 요청] POST /api/kiosk/claim │
|
|
│ │ │
|
|
│ ├──────────────────────────────────────┐ │
|
|
│ │ │ │
|
|
│ ▼ ▼ │
|
|
│ [SQLite: mileage.db] [백그라운드 스레드] │
|
|
│ - claim_tokens _generate_upsell_recommendation()
|
|
│ - users │ │
|
|
│ │ │
|
|
│ ┌────────────────────┼────────────────┐ │
|
|
│ │ ▼ │ │
|
|
│ │ ┌─────────────────────┐ │ │
|
|
│ │ │ 데이터 수집 │ │ │
|
|
│ │ ├─────────────────────┤ │ │
|
|
│ │ │ 1. 현재 구매 품목 │ │ │
|
|
│ │ │ 2. 고객 구매 이력 │ │ │
|
|
│ │ │ 3. 약국 보유 제품 │ │ │
|
|
│ │ └──────────┬──────────┘ │ │
|
|
│ │ │ │ │
|
|
│ │ ▼ │ │
|
|
│ │ ┌─────────────────────┐ │ │
|
|
│ │ │ Clawdbot Gateway │ │ │
|
|
│ │ │ (WebSocket) │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ Model: Sonnet │ │ │
|
|
│ │ │ (비용 최적화) │ │ │
|
|
│ │ └──────────┬──────────┘ │ │
|
|
│ │ │ │ │
|
|
│ │ ▼ │ │
|
|
│ │ ┌─────────────────────┐ │ │
|
|
│ │ │ Claude AI 응답 │ │ │
|
|
│ │ │ {product, reason, │ │ │
|
|
│ │ │ message} │ │ │
|
|
│ │ └──────────┬──────────┘ │ │
|
|
│ │ │ │ │
|
|
│ └───────────────────┼─────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────┐ │
|
|
│ │ SQLite: ai_recommendations │
|
|
│ │ - recommended_product │ │
|
|
│ │ - recommendation_message│ │
|
|
│ │ - trigger_products │ │
|
|
│ │ - expires_at │ │
|
|
│ └──────────┬──────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────┐ │
|
|
│ │ 마이페이지 / 키오스크 │ │
|
|
│ │ 추천 카드 노출 │ │
|
|
│ └─────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 데이터 흐름 상세
|
|
|
|
### 1단계: 트리거 (키오스크 적립)
|
|
|
|
```python
|
|
# POST /api/kiosk/claim
|
|
# 고객이 전화번호로 마일리지 적립 요청
|
|
|
|
# 적립 완료 후 백그라운드에서 AI 추천 생성
|
|
threading.Thread(target=_bg_upsell, daemon=True).start()
|
|
```
|
|
|
|
**포인트:** 적립 응답은 즉시 반환, AI 추천은 백그라운드에서 처리 (non-blocking)
|
|
|
|
---
|
|
|
|
### 2단계: 데이터 수집
|
|
|
|
#### 2-1. 현재 구매 품목
|
|
|
|
```python
|
|
# 키오스크 트리거 시 전달받은 sale_items에서 추출
|
|
current_items = ', '.join(item['name'] for item in sale_items)
|
|
# 예: "타이레놀, 판피린, 비타민C"
|
|
```
|
|
|
|
#### 2-2. 고객 구매 이력 (최근 5건)
|
|
|
|
```sql
|
|
-- SQLite: 최근 적립한 거래 ID 조회
|
|
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
|
|
|
|
-- MSSQL: 각 거래의 품목 조회
|
|
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
|
|
```
|
|
|
|
#### 2-3. 약국 보유 제품 목록 (TOP 40)
|
|
|
|
```sql
|
|
-- MSSQL: 최근 30일 판매 상위 40개 제품
|
|
SELECT TOP 40
|
|
ISNULL(G.GoodsName, '') AS name,
|
|
COUNT(*) as sales,
|
|
MAX(G.Saleprice) as price
|
|
FROM SALE_SUB S
|
|
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
|
|
WHERE S.SL_DT_appl >= CONVERT(VARCHAR(8), DATEADD(DAY, -30, GETDATE()), 112)
|
|
AND G.GoodsName IS NOT NULL
|
|
AND G.GoodsName NOT LIKE N'%(판매불가)%'
|
|
GROUP BY G.GoodsName
|
|
ORDER BY COUNT(*) DESC
|
|
```
|
|
|
|
**왜 TOP 40?**
|
|
- AI 컨텍스트 토큰 절약
|
|
- 실제로 많이 팔리는 제품만 추천 (재고 있음 보장)
|
|
- 판매불가 제품 자동 제외
|
|
|
|
---
|
|
|
|
### 3단계: AI 프롬프트 구성
|
|
|
|
```python
|
|
UPSELL_MODEL = 'anthropic/claude-sonnet-4-5' # Opus 대신 Sonnet (비용 최적화)
|
|
|
|
SYSTEM_PROMPT = """당신은 동네 약국(청춘약국)의 친절한 약사입니다.
|
|
고객의 구매 이력을 보고, 약국에 실제로 있는 제품 중에서 하나를 추천합니다.
|
|
반드시 [약국 보유 제품 목록]에 있는 제품명을 그대로 사용하세요.
|
|
목록에 없는 제품은 절대 추천하지 마세요.
|
|
강압적이거나 광고 같은 느낌이 아닌, 진심으로 건강을 걱정하는 약사의 말투로 작성해주세요.
|
|
반드시 아래 JSON 형식으로만 응답하세요."""
|
|
|
|
USER_PROMPT = f"""고객 이름: {user_name}
|
|
오늘 구매한 약: {current_items}
|
|
최근 구매 이력: {recent_products}
|
|
|
|
[약국 보유 제품 목록 — 이 중에서만 추천하세요]
|
|
{product_list}
|
|
|
|
규칙:
|
|
1. 위 목록에 있는 제품 중 오늘 구매한 약과 함께 먹으면 좋거나, 구매 패턴상 필요해보이는 약 1가지만 추천
|
|
2. 오늘 이미 구매한 제품은 추천하지 마세요
|
|
3. 메시지는 2문장 이내, 따뜻하고 자연스러운 톤
|
|
4. product 필드에는 목록에 있는 제품명을 정확히 그대로 적어주세요
|
|
|
|
응답 JSON:
|
|
{{"product": "목록에 있는 정확한 제품명", "reason": "추천 이유 (내부용)", "message": "고객용 메시지"}}"""
|
|
```
|
|
|
|
---
|
|
|
|
### 4단계: AI 응답 및 저장
|
|
|
|
```json
|
|
// Claude 응답 예시
|
|
{
|
|
"product": "종근당 비타민D 1000IU",
|
|
"reason": "감기약과 함께 면역력 강화에 도움",
|
|
"message": "홍길동님, 감기약 드시면서 비타민D도 같이 챙기시면 회복에 도움이 되실 거예요. 요즘 일조량 적을 때 특히 좋답니다."
|
|
}
|
|
```
|
|
|
|
```sql
|
|
-- SQLite: ai_recommendations 테이블에 저장
|
|
INSERT INTO ai_recommendations
|
|
(user_id, transaction_id, recommended_product, recommendation_message,
|
|
recommendation_reason, trigger_products, ai_raw_response, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
```
|
|
|
|
---
|
|
|
|
### 5단계: 추천 노출
|
|
|
|
```
|
|
GET /api/recommendation/{user_id}
|
|
```
|
|
|
|
- 마이페이지에서 조회
|
|
- 키오스크에서 적립 직후 표시
|
|
- 7일 후 만료 (expires_at)
|
|
|
|
---
|
|
|
|
## 핵심 쿼리 정리
|
|
|
|
| 용도 | DB | 쿼리 |
|
|
|------|-----|------|
|
|
| 고객 최근 거래 | SQLite | `claim_tokens WHERE claimed_by_user_id = ?` |
|
|
| 거래별 품목 | MSSQL | `SALE_SUB JOIN CD_GOODS WHERE SL_NO_order = ?` |
|
|
| 보유 제품 TOP 40 | MSSQL | `SALE_SUB GROUP BY GoodsName ORDER BY COUNT DESC` |
|
|
| 추천 저장 | SQLite | `INSERT INTO ai_recommendations` |
|
|
| 추천 조회 | SQLite | `SELECT FROM ai_recommendations WHERE user_id = ?` |
|
|
|
|
---
|
|
|
|
## 비용 최적화 전략
|
|
|
|
### 1. 모델 선택
|
|
|
|
```python
|
|
# 업셀링은 Sonnet (빠르고 저렴)
|
|
UPSELL_MODEL = 'anthropic/claude-sonnet-4-5'
|
|
|
|
# 복잡한 분석은 Opus (메인 세션)
|
|
# sessions.patch로 세션별 모델 오버라이드
|
|
```
|
|
|
|
### 2. 토큰 절약
|
|
|
|
- 보유 제품 TOP 40개만 전달 (전체 재고 X)
|
|
- 시스템 프롬프트 간결하게
|
|
- JSON 응답 강제 (불필요한 설명 제거)
|
|
|
|
### 3. 세션 분리
|
|
|
|
```python
|
|
# 고객별 세션 분리 → 컨텍스트 축적 방지
|
|
session_id = f'upsell-real-{user_name}'
|
|
```
|
|
|
|
---
|
|
|
|
## Fallback 전략
|
|
|
|
```python
|
|
# 1차 시도: 실데이터 기반 (보유 제품 목록 제공)
|
|
rec = generate_upsell_real(user_name, current_items, recent_products, available)
|
|
|
|
# 2차 시도: 자유 생성 (보유 제품 목록 없이)
|
|
if not rec:
|
|
rec = generate_upsell(user_name, current_items, recent_products)
|
|
```
|
|
|
|
**왜 Fallback?**
|
|
- MSSQL 연결 실패 시에도 추천 가능
|
|
- 보유 제품 쿼리 실패해도 서비스 지속
|
|
|
|
---
|
|
|
|
## 관련 파일
|
|
|
|
```
|
|
pharmacy-pos-qr-system/
|
|
├── backend/
|
|
│ ├── app.py
|
|
│ │ ├── _get_available_products() # 보유 제품 조회
|
|
│ │ ├── _generate_upsell_recommendation() # 메인 로직
|
|
│ │ └── /api/recommendation/{user_id} # 추천 조회 API
|
|
│ │
|
|
│ ├── services/
|
|
│ │ └── clawdbot_client.py
|
|
│ │ ├── generate_upsell() # 자유 생성
|
|
│ │ ├── generate_upsell_real() # 실데이터 기반
|
|
│ │ └── ask_clawdbot() # Gateway 호출
|
|
│ │
|
|
│ ├── templates/
|
|
│ │ └── admin_ai_crm.html # CRM 관리 페이지
|
|
│ │
|
|
│ └── db/
|
|
│ └── mileage.db # SQLite (ai_recommendations)
|
|
│
|
|
└── docs/
|
|
├── ai-upselling-architecture.md # 이 문서
|
|
└── clawdbot-gateway-api.md # Gateway 연동 가이드
|
|
```
|
|
|
|
---
|
|
|
|
## 향후 개선 방향
|
|
|
|
### 1. 추천 정확도 향상
|
|
- 제품 카테고리 분류 추가 (감기약, 영양제, 외용제 등)
|
|
- 계절/시간대별 추천 가중치
|
|
- 고객 연령대/성별 기반 필터
|
|
|
|
### 2. 성과 측정
|
|
- 추천 → 실제 구매 전환율 추적
|
|
- A/B 테스트 (추천 vs 비추천)
|
|
- 인기 추천 제품 통계
|
|
|
|
### 3. 실시간 재고 연동
|
|
- 현재: 최근 30일 판매 기준 (간접 재고)
|
|
- 개선: 실제 재고 수량 기반 추천
|
|
|
|
### 4. 멀티 추천
|
|
- 현재: 1개 제품만 추천
|
|
- 개선: 상황별 2-3개 옵션 제시
|
|
|
|
---
|
|
|
|
*작성: 2026-02-27 | 용림 🐉*
|