pharmacy-pos-qr-system/docs/후향적적립QR_POS만들기.md
시골약사 a9041e9c9e feat: 프로젝트 초기 구조 설정
- PyQt5 POS 판매 조회 GUI (Phase 1 완료)
- Flask API 서버 스켈레톤 (Phase 2 준비)
- SQLite 마일리지 DB 스키마 설계
- 프로젝트 문서 및 README 추가
- 기본 디렉터리 구조 생성

Phase 1: POS 판매 내역 조회 GUI 완료
Phase 2: QR 토큰 생성 및 마일리지 적립 (예정)
Phase 3: 카카오 로그인 연동 (예정)
Phase 4: 마일리지 시스템 완성 (예정)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-23 13:59:00 +09:00

836 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 후향적 고객 매핑 및 마일리지 적립 시스템 기획서
> **버전**: 1.0
> **작성일**: 2026-01-23
> **목적**: POS 판매 후 QR 코드 + 카카오 로그인을 통한 후향적 고객 매핑 및 마일리지 적립 시스템
---
## 1. 개요
### 1-1. 배경 및 문제점
현재 약국 POS 시스템의 고객 데이터 현황:
| 구분 | 비율 | 설명 |
|------|------|------|
| **비고객 판매** | ~80% | 고객 정보 없이 판매 (SL_CD_custom = NULL) |
| **이름만 기록** | ~15% | 고객명만 있고 코드 없음 (SL_NM_custom만 기록) |
| **완전 매핑** | ~5% | 고객코드로 CD_PERSON과 연결됨 |
**문제점**:
- 대부분의 거래에서 구매자 정보가 누락됨
- 단골 고객 분석/마케팅 불가
- 고객 충성도 프로그램 운영 어려움
### 1-2. 솔루션 개요
**핵심 컨셉**: 영수증 QR → 카카오 로그인 → 후향적 고객 매핑 → 마일리지 적립
```
┌─────────────────────────────────────────────────────────────┐
│ 후향적 고객 매핑 흐름 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [POS 판매] │
│ ↓ │
│ 영수증/라벨에 QR 인쇄 (claim_token 포함) │
│ ↓ │
│ (시간 경과... 고객이 나중에 QR 촬영) │
│ ↓ │
│ 웹앱 랜딩 → 카카오 간편로그인 │
│ ↓ │
│ 토큰 검증 → 거래(transaction) 매핑 │
│ ↓ │
│ ✅ 마일리지 적립 + CD_PERSON 연결 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 2. 현재 DB 구조 분석
### 2-1. 데이터베이스 구성
```
┌──────────────────────────────────────────────────────────────┐
│ PM_PRES DB (판매 데이터) │
├──────────────────────────────────────────────────────────────┤
│ │
│ SALE_MAIN (판매 주문 헤더) │
│ ├── SL_NO_order: 주문번호 (예: 20251024000002) ← 고유값 │
│ ├── SL_DAY_SERIAL: 당일 거래 순번 (1, 2, 3...) │
│ ├── SL_DT_appl: 판매일자 (YYYYMMDD) │
│ ├── SL_NM_custom: 고객명 (대부분 NULL) │
│ ├── SL_CD_custom: 고객코드 → CD_PERSON.CUSCODE │
│ ├── SL_MY_total: 총 매출액 │
│ ├── SL_MY_discount: 할인액 │
│ └── InsertTime: 등록시간 │
│ │ │
│ └──> SALE_SUB (판매 상세 품목) │
│ ├── SL_NO_order: 주문번호 (FK) │
│ ├── DrugCode: 약품코드 │
│ ├── SL_TOTAL_PRICE: 판매가 │
│ ├── SL_MY_in_cost: 매입가 │
│ └── SL_NM_item: 수량 │
│ │
│ CD_SUNAB (결제 정보) │
│ ├── PRESERIAL: 주문번호 참조 ← SALE_MAIN.SL_NO_order │
│ ├── OTC_CARD: 카드 결제금액 │
│ └── OTC_CASH: 현금 결제금액 │
│ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ PM_BASE DB (고객 마스터) │
├──────────────────────────────────────────────────────────────┤
│ │
│ CD_PERSON (고객 정보) │
│ ├── CUSCODE: 고객코드 (PK) ← SALE_MAIN.SL_CD_custom │
│ ├── PANAME: 고객명 │
│ ├── PANUM: 주민번호 (개인식별) │
│ ├── TEL_NO: 전화번호 1 (집) │
│ ├── PHONE: 전화번호 2 (휴대폰) │
│ ├── PHONE2: 전화번호 3 (대체) │
│ └── CUSETC: 고객 메모/특이사항 (2000자) │
│ │
└──────────────────────────────────────────────────────────────┘
```
### 2-2. 테이블 상세 구조
#### SALE_MAIN (판매 주문 헤더)
| 컬럼명 | 타입 | 설명 | 예시 |
|--------|------|------|------|
| SL_NO_order | VARCHAR(20) | 주문번호 (PK) | `20251024000002` |
| SL_DAY_SERIAL | INT | 당일 거래 순번 | `1`, `2`, `3`... |
| SL_DT_appl | VARCHAR(8) | 판매일자 | `20251024` |
| SL_NM_custom | VARCHAR(50) | 고객명 | `김철수` 또는 NULL |
| SL_CD_custom | VARCHAR(10) | 고객코드 | `0000012345` 또는 NULL |
| SL_MY_total | DECIMAL | 총 매출액 | `50000` |
| SL_MY_discount | DECIMAL | 할인액 | `5000` |
| SL_MY_sale | DECIMAL | 실 판매액 | `45000` |
| InsertTime | DATETIME | 등록시간 | `2025-10-24 14:30:00` |
#### CD_PERSON (고객 정보)
| 컬럼명 | 타입 | 설명 | 예시 |
|--------|------|------|------|
| CUSCODE | VARCHAR(10) | 고객코드 (PK) | `0000012345` |
| INSCODE | VARCHAR(10) | 기관코드 | `0000000001` |
| SEQ | INT | 순번 | `1` |
| INDATE | VARCHAR(8) | 등록일 | `20251024` |
| PANAME | VARCHAR(20) | 고객명 | `김철수` |
| PANUM | VARCHAR(13) | 주민번호 | `800101-1******` |
| TEL_NO | VARCHAR(20) | 전화번호 1 | `02-123-4567` |
| PHONE | VARCHAR(20) | 휴대폰 | `010-1234-5678` |
| PHONE2 | VARCHAR(20) | 대체번호 | `010-9876-5432` |
| CUSETC | VARCHAR(2000) | 메모 | `당뇨병 주의` |
### 2-3. 현재 고객-판매 연결 방식
```sql
-- 고객이 식별된 판매 (약 5%)
SELECT
M.SL_NO_order,
M.SL_NM_custom, -- '김철수'
M.SL_CD_custom, -- '0000012345'
P.PANAME, -- CD_PERSON에서 조회
P.PHONE -- 전화번호
FROM SALE_MAIN M
LEFT JOIN CD_PERSON P ON M.SL_CD_custom = P.CUSCODE
WHERE M.SL_CD_custom IS NOT NULL
AND M.SL_CD_custom != ''
-- 비고객 판매 (약 80%)
SELECT * FROM SALE_MAIN
WHERE SL_CD_custom IS NULL OR SL_CD_custom = ''
```
---
## 3. 신규 테이블 설계 (SQLite - mileage.db)
### 3-1. DB 분리 구조
```
┌─────────────────────────┐ ┌─────────────────────────────┐
│ MSSQL (기존) │ │ SQLite (신규: mileage.db) │
├─────────────────────────┤ ├─────────────────────────────┤
│ │ │ │
│ PM_PRES │ │ users │
│ ├── SALE_MAIN ─────────┼──────────┼─→ kakao_user_id (카카오 키) │
│ ├── SALE_SUB │ 주문번호 │ │
│ └── CD_SUNAB │ 연결 │ customer_identities │
│ │ │ └── provider='kakao' │
├─────────────────────────┤ │ │
│ PM_BASE │ │ claim_tokens │
│ └── CD_PERSON ─────────┼──────────┼─→ transaction_id (주문번호) │
│ (CUSCODE) │ 고객코드 │ └── token_hash │
│ │ 연결 │ │
└─────────────────────────┘ │ mileage_ledger │
│ ├── user_id │
│ ├── transaction_id │
│ └── points (+/-) │
│ │
│ pos_customer_links │
│ ├── user_id │
│ └── cuscode (CD_PERSON) │
│ │
└─────────────────────────────┘
```
### 3-2. 테이블 DDL (SQLite)
#### users (카카오 로그인 계정)
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nickname VARCHAR(100),
profile_image_url VARCHAR(500),
email VARCHAR(200),
is_email_verified BOOLEAN DEFAULT FALSE,
phone VARCHAR(20), -- 직접 입력 또는 카카오 (비즈앱)
mileage_balance INTEGER DEFAULT 0, -- 잔액 캐시 (성능용)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
#### customer_identities (외부 로그인 매핑)
```sql
CREATE TABLE customer_identities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
provider VARCHAR(20) NOT NULL, -- 'kakao', 'naver', 'google' 등
provider_user_id VARCHAR(100) NOT NULL, -- 카카오 user id
provider_data TEXT, -- JSON (추가 정보)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(provider, provider_user_id) -- 중복 방지
);
CREATE INDEX idx_identities_user ON customer_identities(user_id);
```
#### claim_tokens (영수증 QR 토큰)
```sql
CREATE TABLE claim_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
transaction_id VARCHAR(20) NOT NULL, -- SALE_MAIN.SL_NO_order
pharmacy_id VARCHAR(20), -- 약국 식별자 (다중 약국 대비)
token_hash VARCHAR(64) NOT NULL, -- SHA256 해시 (원문 저장 X)
total_amount INTEGER NOT NULL, -- 거래 금액
claimable_points INTEGER NOT NULL, -- 적립 가능 포인트
expires_at DATETIME NOT NULL, -- 만료시간 (14~30일)
claimed_at DATETIME, -- 적립 완료 시간
claimed_by_user_id INTEGER REFERENCES users(id),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(transaction_id), -- 1거래 1토큰
UNIQUE(token_hash)
);
CREATE INDEX idx_tokens_hash ON claim_tokens(token_hash);
CREATE INDEX idx_tokens_expires ON claim_tokens(expires_at);
```
#### mileage_ledger (마일리지 원장)
```sql
CREATE TABLE mileage_ledger (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
transaction_id VARCHAR(20), -- SALE_MAIN.SL_NO_order (적립 시)
points INTEGER NOT NULL, -- + 적립, - 사용
balance_after INTEGER NOT NULL, -- 거래 후 잔액
reason VARCHAR(50) NOT NULL, -- 'PURCHASE_CLAIM', 'POINT_USE', 'EVENT_BONUS' 등
description TEXT, -- 상세 설명
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(transaction_id) -- 1거래 1회 적립 보장
);
CREATE INDEX idx_ledger_user ON mileage_ledger(user_id);
CREATE INDEX idx_ledger_transaction ON mileage_ledger(transaction_id);
```
#### pos_customer_links (POS 고객 ↔ 카카오 계정 연결)
```sql
CREATE TABLE pos_customer_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
pharmacy_id VARCHAR(20), -- 약국 식별자
cuscode VARCHAR(10), -- CD_PERSON.CUSCODE (기존 고객코드)
customer_name VARCHAR(50), -- 고객명 (캐시)
linked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
UNIQUE(user_id, pharmacy_id) -- 약국별 1:1 연결
);
CREATE INDEX idx_links_cuscode ON pos_customer_links(cuscode);
```
---
## 4. 후향적 매핑 흐름 상세
### 4-1. 전체 시퀀스 다이어그램
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ POS │ │ 영수증 │ │ 고객 │ │ 웹앱 │ │ 서버 │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │
│ 1. 판매 완료 │ │ │ │
│───────────────┼───────────────┼───────────────┼──────────────>│
│ │ │ │ │
│ │ 2. QR 인쇄 │ │ │
│ │<──────────────┼───────────────┼───────────────│
│ │ (claim_token) │ │ │
│ │ │ │ │
│ │ │ 3. QR 촬영 │ │
│ │ │──────────────>│ │
│ │ │ │ │
│ │ │ │ 4. 토큰 검증 │
│ │ │ │──────────────>│
│ │ │ │ │
│ │ │ │ 5. 카카오로그인│
│ │ │ │<─────────────>│
│ │ │ │ │
│ │ │ │ 6. 적립 요청 │
│ │ │ │──────────────>│
│ │ │ │ │
│ │ │ │ 7. 마일리지 │
│ │ │ │ 적립 완료 │
│ │ │ │<──────────────│
│ │ │ │ │
```
### 4-2. 단계별 상세
#### Step 1: POS 판매 완료
```python
# MSSQL SALE_MAIN에 INSERT 발생
# 주문번호(SL_NO_order) 생성: 20251024000042
```
#### Step 2: QR 토큰 생성 및 인쇄
```python
# 토큰 생성 로직
import hashlib
import secrets
import datetime
def generate_claim_token(transaction_id, total_amount, mileage_rate=0.03):
# 1. 랜덤 nonce 생성
nonce = secrets.token_hex(16)
# 2. 토큰 원문 생성
token_raw = f"{transaction_id}:{nonce}:{datetime.datetime.now().isoformat()}"
# 3. 해시 생성 (원문 저장 X)
token_hash = hashlib.sha256(token_raw.encode()).hexdigest()
# 4. QR URL 생성
qr_url = f"https://pharmacy.example.com/claim?t={token_raw}"
# 5. 적립 포인트 계산
claimable_points = int(total_amount * mileage_rate)
# 6. DB 저장
# INSERT INTO claim_tokens (transaction_id, token_hash, total_amount,
# claimable_points, expires_at)
# VALUES (?, ?, ?, ?, datetime('now', '+30 days'))
return qr_url
```
```
QR 코드 내용 (URL):
https://pharmacy.example.com/claim?t=20251024000042:a1b2c3d4e5f6:2025-10-24T14:30:00
영수증 출력 예시:
┌─────────────────────────────────┐
│ 양구청춘약국 │
│ 2025-10-24 14:30 │
│ │
│ 타이레놀 500mg ×2 3,000원 │
│ 밴드 ×1 2,000원 │
│ ─────────────────────────────│
│ 합계 5,000원 │
│ │
│ [QR 코드] │
│ │
│ QR 촬영하고 │
│ 150P 적립받으세요! │
│ (유효기간: 30일) │
└─────────────────────────────────┘
```
#### Step 3~4: QR 촬영 및 토큰 검증
```python
@app.route('/claim', methods=['GET'])
def claim_landing():
token = request.args.get('t')
# 토큰 파싱
parts = token.split(':')
transaction_id = parts[0]
# 해시 계산
token_hash = hashlib.sha256(token.encode()).hexdigest()
# DB 검증
claim = db.query("""
SELECT * FROM claim_tokens
WHERE token_hash = ?
AND expires_at > datetime('now')
AND claimed_at IS NULL
""", [token_hash])
if not claim:
return render_template('claim_error.html',
error="만료되었거나 이미 사용된 영수증입니다.")
# 세션에 토큰 정보 저장
session['claim_token'] = token
session['claim_info'] = {
'transaction_id': claim['transaction_id'],
'claimable_points': claim['claimable_points']
}
# 로그인 페이지로 리다이렉트
return redirect('/auth/kakao/login')
```
#### Step 5: 카카오 로그인
```python
@app.route('/auth/kakao/callback')
def kakao_callback():
code = request.args.get('code')
# 액세스 토큰 발급
access_token = kakao_api.get_access_token(code)
# 사용자 정보 조회
kakao_user = kakao_api.get_user_info(access_token)
# {
# "id": 1234567890,
# "kakao_account": {
# "email": "user@example.com",
# "profile": {"nickname": "홍길동"}
# }
# }
# users 테이블에서 찾기 또는 생성
user = get_or_create_user(
provider='kakao',
provider_user_id=str(kakao_user['id']),
nickname=kakao_user['kakao_account']['profile']['nickname'],
email=kakao_user['kakao_account'].get('email')
)
# 세션에 user_id 저장
session['user_id'] = user['id']
# 클레임 진행 중이면 적립 페이지로
if session.get('claim_token'):
return redirect('/claim/confirm')
return redirect('/mypage')
```
#### Step 6~7: 마일리지 적립
```python
@app.route('/claim/confirm', methods=['POST'])
def claim_confirm():
user_id = session.get('user_id')
claim_info = session.get('claim_info')
if not user_id or not claim_info:
return jsonify({'error': '세션이 만료되었습니다.'}), 400
transaction_id = claim_info['transaction_id']
points = claim_info['claimable_points']
try:
with db.transaction():
# 1. 토큰 사용 처리
db.execute("""
UPDATE claim_tokens
SET claimed_at = datetime('now'),
claimed_by_user_id = ?
WHERE transaction_id = ?
AND claimed_at IS NULL
""", [user_id, transaction_id])
# 2. 마일리지 적립
current_balance = db.query(
"SELECT mileage_balance FROM users WHERE id = ?",
[user_id]
)['mileage_balance']
new_balance = current_balance + points
db.execute("""
INSERT INTO mileage_ledger
(user_id, transaction_id, points, balance_after, reason, description)
VALUES (?, ?, ?, ?, 'PURCHASE_CLAIM', ?)
""", [user_id, transaction_id, points, new_balance,
f"영수증 QR 적립 ({transaction_id})"])
# 3. 잔액 업데이트
db.execute("""
UPDATE users SET mileage_balance = ? WHERE id = ?
""", [new_balance, user_id])
# 4. POS 고객 연결 (선택적)
link_pos_customer(user_id, transaction_id)
# 세션 정리
session.pop('claim_token', None)
session.pop('claim_info', None)
return jsonify({
'success': True,
'points_earned': points,
'new_balance': new_balance
})
except sqlite3.IntegrityError:
# transaction_id UNIQUE 제약 위반 = 이미 적립됨
return jsonify({'error': '이미 적립된 영수증입니다.'}), 400
```
---
## 5. API 설계
### 5-1. 토큰 관련 API
#### POST /api/claim/token/generate
영수증 QR 토큰 생성 (POS에서 호출)
**Request**:
```json
{
"transaction_id": "20251024000042",
"total_amount": 50000,
"pharmacy_id": "YANGGU001"
}
```
**Response**:
```json
{
"success": true,
"qr_url": "https://pharmacy.example.com/claim?t=...",
"claimable_points": 1500,
"expires_at": "2025-11-23T14:30:00"
}
```
#### GET /api/claim?t={token}
토큰 검증 및 정보 조회
**Response (유효)**:
```json
{
"valid": true,
"transaction_id": "20251024000042",
"claimable_points": 1500,
"total_amount": 50000,
"expires_at": "2025-11-23T14:30:00"
}
```
**Response (무효)**:
```json
{
"valid": false,
"error": "expired|claimed|not_found"
}
```
#### POST /api/claim/confirm
적립 실행 (로그인 후)
**Request**:
```json
{
"token": "20251024000042:a1b2c3d4:..."
}
```
**Response**:
```json
{
"success": true,
"points_earned": 1500,
"new_balance": 3500,
"transaction_id": "20251024000042"
}
```
### 5-2. 마일리지 조회 API
#### GET /api/me/mileage
내 마일리지 잔액 조회
**Response**:
```json
{
"user_id": 123,
"nickname": "홍길동",
"mileage_balance": 3500,
"total_earned": 10000,
"total_used": 6500
}
```
#### GET /api/me/mileage/ledger
적립/사용 내역 조회
**Query Parameters**:
- `limit`: 조회 개수 (기본 20)
- `offset`: 페이지네이션
**Response**:
```json
{
"ledger": [
{
"id": 45,
"points": 1500,
"balance_after": 3500,
"reason": "PURCHASE_CLAIM",
"description": "영수증 QR 적립 (20251024000042)",
"created_at": "2025-10-24T15:30:00"
},
{
"id": 44,
"points": -2000,
"balance_after": 2000,
"reason": "POINT_USE",
"description": "결제 시 사용",
"created_at": "2025-10-20T10:00:00"
}
],
"total_count": 25,
"has_more": true
}
```
### 5-3. 인증 API
#### GET /auth/kakao/login
카카오 로그인 페이지로 리다이렉트
#### GET /auth/kakao/callback
카카오 콜백 처리
#### POST /auth/logout
로그아웃
---
## 6. 보안 및 부정 사용 방지
### 6-1. 토큰 보안
| 보안 요소 | 구현 방법 |
|-----------|-----------|
| **1회성 사용** | `claimed_at IS NULL` 체크 후 즉시 업데이트 |
| **만료 시간** | `expires_at` 필드로 14~30일 제한 |
| **해시 저장** | 토큰 원문 저장 X, SHA256 해시만 저장 |
| **중복 적립 방지** | `mileage_ledger.transaction_id UNIQUE` |
### 6-2. 부정 사용 시나리오 및 대응
| 시나리오 | 대응 방법 |
|----------|-----------|
| QR 사진 공유 | 1회성 토큰 + 로그인 필수 |
| 토큰 위조 | 서버 서명 검증 (HMAC) |
| 중복 적립 시도 | DB UNIQUE 제약 |
| 만료 토큰 사용 | `expires_at` 검증 |
| 타인 영수증 도용 | (완벽 방지 어려움) 영수증 일부 숫자 확인 옵션 |
### 6-3. 적립률 정책 예시
```python
MILEAGE_CONFIG = {
'default_rate': 0.03, # 기본 3%
'vip_rate': 0.05, # VIP 5%
'min_points': 10, # 최소 적립 10P
'max_points': 10000, # 최대 적립 10,000P
'expiry_days': 30, # 토큰 유효기간 30일
'excluded_categories': [], # 적립 제외 품목 (있으면)
}
```
---
## 7. POS 관리화면 연동
### 7-1. 거래 상세 화면
```
┌─────────────────────────────────────────────────────────────┐
│ 거래 상세 - 20251024000042 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 판매일시: 2025-10-24 14:30:00 │
│ 총 금액: 50,000원 │
│ 결제 방법: 카드 │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 멤버십 상태: ✅ 연결됨 ││
│ │ 연결 고객: 홍길동 (카카오) ││
│ │ 연결 일시: 2025-10-24 16:00:00 ││
│ │ 적립 포인트: 1,500P ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ 품목: │
│ - 타이레놀 500mg × 2 3,000원 │
│ - 밴드 × 1 2,000원 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 7-2. 고객 목록 필터
```
┌─────────────────────────────────────────────────────────────┐
│ 고객 관리 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 필터: [전체 ▼] [카카오 연결 고객만 ☑] │
│ │
│ ┌───────┬──────────┬─────────────┬───────────┬───────────┐ │
│ │ 고객명 │ 카카오 연결│ 마일리지 잔액│ 총 구매액 │ 방문 횟수 │ │
│ ├───────┼──────────┼─────────────┼───────────┼───────────┤ │
│ │ 홍길동 │ ✅ │ 3,500P │ 150,000원 │ 12회 │ │
│ │ 김철수 │ ✅ │ 1,200P │ 80,000원 │ 5회 │ │
│ │ 이영희 │ ❌ │ - │ 200,000원 │ 20회 │ │
│ └───────┴──────────┴─────────────┴───────────┴───────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 7-3. CD_PERSON 연동
```python
def link_pos_customer(user_id, transaction_id):
"""거래 정보를 기반으로 POS 고객과 연결"""
# 1. MSSQL에서 거래 정보 조회
sale = mssql_query("""
SELECT SL_CD_custom, SL_NM_custom
FROM SALE_MAIN
WHERE SL_NO_order = ?
""", [transaction_id])
if not sale or not sale['SL_CD_custom']:
# 고객코드가 없으면 연결 스킵
return
cuscode = sale['SL_CD_custom']
customer_name = sale['SL_NM_custom']
# 2. pos_customer_links에 저장
sqlite_execute("""
INSERT OR IGNORE INTO pos_customer_links
(user_id, cuscode, customer_name)
VALUES (?, ?, ?)
""", [user_id, cuscode, customer_name])
```
---
## 8. 카카오 로그인 정책 참고
### 8-1. 수집 가능한 정보
| 정보 | 기본 제공 | 추가 심사 필요 |
|------|-----------|----------------|
| kakao_user_id | ✅ | - |
| 닉네임 | ✅ (동의 시) | - |
| 프로필 이미지 | ✅ (동의 시) | - |
| 이메일 | ⚠️ (동의 시) | - |
| 성별/연령대 | - | ✅ |
| **전화번호** | - | ✅ (비즈앱) |
### 8-2. 전화번호 수집 방법
1. **카카오 비즈앱 전환** 필요
2. **전화번호 권한 심사** 신청
3. 로그인 시 **명시적 동의** 팝업
4. 서비스 이용 목적, 개인정보처리방침 제출
**권장**:
- 초기에는 `kakao_user_id`만 사용
- 전화번호는 직접 입력 UI로 수집 (선택)
- 규모 확대 후 비즈앱 심사 진행
---
## 9. 향후 확장 계획
### 9-1. Phase 1 (MVP)
- [x] SQLite 테이블 설계
- [ ] 영수증 QR 생성 API
- [ ] 카카오 로그인 연동
- [ ] 마일리지 적립 기능
- [ ] 내 마일리지 조회 페이지
### 9-2. Phase 2 (확장)
- [ ] 마일리지 사용 (결제 시 차감)
- [ ] POS 관리화면 연동
- [ ] 이벤트 보너스 포인트
- [ ] 푸시 알림 (적립 완료)
### 9-3. Phase 3 (고도화)
- [ ] 다중 약국 지원
- [ ] 알림톡 연동 (적립 완료 알림)
- [ ] VIP 등급제
- [ ] 포인트 양도/선물
---
## 10. 참고: 기존 코드 위치
| 기능 | 파일 위치 | 비고 |
|------|-----------|------|
| 카카오 로그인 | `full/src/kakao_login_example.py` | KakaoLoginAPI 클래스 |
| 카카오 설정 | `full/src/kakao_config.py` | REST_API_KEY 등 |
| QR 코드 생성 | `print_label.py` | Brother QL-710W 프린터 |
| MSSQL 연결 | `dbsetup.py` | PM_PRES, PM_BASE 연결 |
| OTC 판매 API | `otc_stats_api.py` | sales-details 엔드포인트 |
| 고객 분석 | `customer_analytics_api.py` | 단골 고객 조회 |
---
## 부록 A: 용어 정리
| 용어 | 설명 |
|------|------|
| **후향적 매핑** | 판매 시점이 아닌, 판매 후 나중에 고객 정보를 연결하는 방식 |
| **claim_token** | 영수증에 인쇄되는 1회성 토큰 (QR 코드에 포함) |
| **mileage_ledger** | 마일리지 적립/사용 내역을 기록하는 원장 테이블 |
| **CUSCODE** | CD_PERSON 테이블의 고객 코드 (기존 POS 고객 식별자) |
| **kakao_user_id** | 카카오 로그인 시 발급되는 고유 사용자 ID |
---
## 부록 B: 관련 문서
- [CLAUDE.md](../CLAUDE.md) - 프로젝트 가이드
- [POS 입고 기능 개선 정리](./pos/관련정리.md) - drug_code, 바코드 구조
- [OTC 통계 API 문서](./dev-guide/otc-stats-api-documentation.md)