feat: 제품 검색 페이지 및 QR 라벨 인쇄 기능
- /admin/products: 전체 제품 검색 페이지 (OTC) - /api/products: 제품 검색 API (세트상품 바코드 포함) - qr_printer.py: Brother QL-710W 프린터 연동 - /api/qr-print, /api/qr-preview: QR 라벨 인쇄/미리보기 API - 판매상세 페이지에 QR 인쇄 버튼 추가 - 수량 선택 UI (+/- 버튼, 최대 10장) - 세트상품 제조사 표시 개선 - 대시보드 헤더에 제품검색/판매조회 탭 추가
This commit is contained in:
324
docs/ai-upselling-architecture.md
Normal file
324
docs/ai-upselling-architecture.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# 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 | 용림 🐉*
|
||||
98
docs/카드현금구분_.md
Normal file
98
docs/카드현금구분_.md
Normal file
@@ -0,0 +1,98 @@
|
||||
.# 팜IT3000 (PIT3000) DB 구조
|
||||
|
||||
## DB 접속 정보
|
||||
- **서버**: 192.168.0.101\PM2014 (MSSQL)
|
||||
- **계정**: sa / tmddls214!%(
|
||||
- **ODBC**: Driver 18 + `OPENSSL_CONF=/root/person-lookup-web-local/openssl_legacy.conf` 필수
|
||||
- **코드 위치**: /root/person-lookup-web-local/ (CT 200)
|
||||
|
||||
## 데이터베이스 목록
|
||||
| DB명 | 용도 |
|
||||
|------|------|
|
||||
| PM_BASE | 환자 정보, 개인정보, 판매마스터 |
|
||||
| PM_PRES | 처방전, 판매(SALE), 수납(CD_SUNAB), 키오스크 |
|
||||
| PM_DRUG | 약품 마스터(CD_GOODS), 창고 거래(WH_sub) |
|
||||
| PM_DUMS | 재고 관리(INVENTORY, NIMS_REALTIME_INVENTORY) |
|
||||
| PM_ALIMI | 알림톡, SMS |
|
||||
| PM_ALDB | 알림 DB |
|
||||
| PM_EDIRECE/PM_EDISEND | EDI 전자문서 |
|
||||
| PM_IMAGE | 약품 이미지 |
|
||||
| PM_JOBLOG | 작업/시스템 로그 |
|
||||
|
||||
## 결제(수납) 테이블 구조
|
||||
|
||||
### CD_SUNAB (PM_PRES) - 핵심 수납 테이블
|
||||
건별 결제 내역. PRESERIAL로 처방과 연결.
|
||||
|
||||
#### 결제 수단 구분 (금액 기반, 단일 구분 컬럼 없음)
|
||||
| 구분 | 카드결제 | 현금결제 | 외상/기타 |
|
||||
|------|---------|---------|----------|
|
||||
| 조제(ETC, 전문의약품) | `ETC_CARD` | `ETC_CASH` | `ETC_PAPER` |
|
||||
| OTC(일반의약품) | `OTC_CARD` | `OTC_CASH` | `OTC_PAPER` |
|
||||
|
||||
**판별법**: 금액이 0보다 크면 해당 결제수단 사용
|
||||
- `ETC_CARD=6100, ETC_CASH=0` → 카드결제
|
||||
- `ETC_CARD=0, ETC_CASH=5100` → 현금결제
|
||||
|
||||
#### 카드 관련 컬럼
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| `PCardName` | 카드사 이름 (KB국민카드, 신한카드 등) |
|
||||
| `pAPPROVAL_NUM` | 카드 승인번호 |
|
||||
| `pCARDINMODE` | 카드 입력 방식 |
|
||||
| `pTRDTYPE` | 거래 유형 (D1 등) |
|
||||
| `pCHK_GUBUN` | 체크 구분 (TASA=타사, KIC 등) |
|
||||
| `Appr_Gubun` | 승인 구분 (9=정상승인, A 등) |
|
||||
| `pCANCEL_NUM` | 취소 승인번호 |
|
||||
| `CANCEL_DATE` | 취소 일시 |
|
||||
|
||||
#### 현금 관련 컬럼
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| `nCASHINMODE` | 현금영수증 입력 방식 (1 등, 대부분 빈값=미발행) |
|
||||
| `nAPPROVAL_NUM` | 현금영수증 승인번호 |
|
||||
| `nCHK_GUBUN` | 현금 체크 구분 (TASA 등) |
|
||||
|
||||
#### 카드사 분포 (PCardName)
|
||||
| 카드사 | 건수 |
|
||||
|--------|------|
|
||||
| KB국민카드 | 6,106 |
|
||||
| NH농협카드 | 5,172 |
|
||||
| 비씨카드사 | 4,900 |
|
||||
| 하나카드 | 4,880 |
|
||||
| 신한카드 | 3,210 |
|
||||
| 삼성카드사 | 2,100 |
|
||||
| 현대카드사 | 1,960 |
|
||||
| 우리카드 | 1,285 |
|
||||
| 롯데카드사 | 837 |
|
||||
| 카카오페이 | 57 |
|
||||
| 모바일상품권 | 11 |
|
||||
|
||||
### CD_SELL_MASTE (PM_BASE) - 판매마스터
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| `CARD_C` | 카드 결제금액 |
|
||||
| `CHASH_C` | 현금 결제금액 |
|
||||
| `PAPER_C` | 외상 금액 |
|
||||
| `P_GUBUN` | 처방 구분 |
|
||||
| `C_GUBUN` | 고객 구분 |
|
||||
|
||||
### SALE_main (PM_PRES) - 판매 메인
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| `SL_MY_sale` | 판매금액 |
|
||||
| `SL_MY_credit` | 외상금액 |
|
||||
| `SL_MY_recive` | 수납금액 |
|
||||
| `POS_GUBUN` | POS 구분 (빈값=일반, C=카드?, G=기타?) |
|
||||
| `PRESERIAL` | 처방번호 (CD_SUNAB과 조인 키) |
|
||||
|
||||
### KIOSK 테이블 (PM_PRES)
|
||||
- `KIOSK_MAIN`: 키오스크 처방 접수
|
||||
- `KIOSK_CARD`: 키오스크 카드결제 (CARD_NM, CARD_NO, APP_NUM 등)
|
||||
- `KIOSK_CARD_PRES`: 키오스크 카드-처방 연결
|
||||
- `KIOSK_SUB`: 키오스크 서브
|
||||
|
||||
## 주요 조인 관계
|
||||
- `CD_SUNAB.PRESERIAL` ↔ `SALE_main.PRESERIAL` (수납-판매 연결)
|
||||
- `CD_SUNAB.CUSCODE` ↔ `CD_PERSON.CUSCODE` (수납-환자 연결, PM_BASE)
|
||||
- `SALE_main.SL_NO_order` ↔ `SALE_sub.SL_NO_order` (판매 메인-서브)
|
||||
Reference in New Issue
Block a user