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

33 KiB
Raw Permalink Blame History

후향적 고객 매핑 및 마일리지 적립 시스템 기획서

버전: 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. 현재 고객-판매 연결 방식

-- 고객이 식별된 판매 (약 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 (카카오 로그인 계정)

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 (외부 로그인 매핑)

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 토큰)

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 (마일리지 원장)

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);
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 판매 완료

# MSSQL SALE_MAIN에 INSERT 발생
# 주문번호(SL_NO_order) 생성: 20251024000042

Step 2: QR 토큰 생성 및 인쇄

# 토큰 생성 로직
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 촬영 및 토큰 검증

@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: 카카오 로그인

@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: 마일리지 적립

@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:

{
    "transaction_id": "20251024000042",
    "total_amount": 50000,
    "pharmacy_id": "YANGGU001"
}

Response:

{
    "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 (유효):

{
    "valid": true,
    "transaction_id": "20251024000042",
    "claimable_points": 1500,
    "total_amount": 50000,
    "expires_at": "2025-11-23T14:30:00"
}

Response (무효):

{
    "valid": false,
    "error": "expired|claimed|not_found"
}

POST /api/claim/confirm

적립 실행 (로그인 후)

Request:

{
    "token": "20251024000042:a1b2c3d4:..."
}

Response:

{
    "success": true,
    "points_earned": 1500,
    "new_balance": 3500,
    "transaction_id": "20251024000042"
}

5-2. 마일리지 조회 API

GET /api/me/mileage

내 마일리지 잔액 조회

Response:

{
    "user_id": 123,
    "nickname": "홍길동",
    "mileage_balance": 3500,
    "total_earned": 10000,
    "total_used": 6500
}

GET /api/me/mileage/ledger

적립/사용 내역 조회

Query Parameters:

  • limit: 조회 개수 (기본 20)
  • offset: 페이지네이션

Response:

{
    "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. 적립률 정책 예시

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 연동

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)

  • 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: 관련 문서