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:
thug0bin
2026-02-27 13:56:26 +09:00
parent f3fa4707ac
commit 9bd2174501
11 changed files with 1950 additions and 8 deletions

View 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 | 용림 🐉*

View 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` (판매 메인-서브)