pharmacy-pos-qr-system/backend/utils/qr_token_generator.py
시골약사 7aad05acb9 feat: QR 토큰 생성 및 라벨 인쇄 모듈 추가
- qr_token_generator.py: SHA256 기반 1회성 토큰 생성
  * 3% 마일리지 적립 정책
  * 30일 유효기간
  * nonce 기반 중복 방지
  * QR_BASE_URL: https://mile.0bin.in/claim
- qr_label_printer.py: Brother QL-810W 라벨 인쇄
  * 800x306px 라벨 이미지 생성
  * QR 코드 + 거래 정보 포함
  * 미리보기 모드 및 프린터 전송 지원
2026-01-23 16:35:56 +09:00

192 lines
5.6 KiB
Python

"""
QR Claim Token 생성 모듈
후향적 적립을 위한 1회성 토큰 생성
"""
import hashlib
import secrets
from datetime import datetime, timedelta
import sys
import os
# DB 연결
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from db.dbsetup import DatabaseManager
# 설정값
MILEAGE_RATE = 0.03 # 3% 적립
TOKEN_EXPIRY_DAYS = 30 # 30일 유효기간
QR_BASE_URL = "https://mile.0bin.in/claim"
def generate_claim_token(transaction_id, total_amount, pharmacy_id="YANGGU001"):
"""
Claim Token 생성 (SHA256 해시 기반)
Args:
transaction_id (str): POS 거래 ID (SALE_MAIN.SL_NO_order)
total_amount (float): 총 판매 금액
pharmacy_id (str): 약국 코드
Returns:
dict: {
'qr_url': QR 코드 URL,
'token_raw': 토큰 원문 (QR에만 포함),
'token_hash': SHA256 해시 (DB 저장용),
'claimable_points': 적립 가능 포인트,
'expires_at': 만료일시
}
보안 정책:
- token_raw는 DB에 저장하지 않음 (QR 코드에만 포함)
- token_hash만 DB 저장 (SHA256, 64자)
- nonce 사용으로 동일 거래도 매번 다른 토큰
URL 최적화:
- nonce: 64자 -> 12자로 단축 (6바이트 = 충분한 보안)
- 타임스탬프: 제거 (DB에만 저장)
- 결과: 약 80자 -> 빠른 QR 인식
"""
# 1. 랜덤 nonce 생성 (6바이트 = 12자 hex) - 대폭 단축!
nonce = secrets.token_hex(6)
# 2. 타임스탬프 (DB 저장용)
timestamp = datetime.now().isoformat()
# 3. 토큰 원문 생성 (검증용 - DB 저장 X)
token_raw = f"{transaction_id}:{nonce}:{timestamp}"
# 4. SHA256 해시 생성 (DB 저장용)
token_hash = hashlib.sha256(token_raw.encode('utf-8')).hexdigest()
# 5. QR URL 생성 (짧게!) - 타임스탬프 제외
qr_token_short = f"{transaction_id}:{nonce}"
qr_url = f"{QR_BASE_URL}?t={qr_token_short}"
# 6. 적립 포인트 계산
claimable_points = calculate_claimable_points(total_amount)
# 7. 만료일 계산 (30일 후)
expires_at = datetime.now() + timedelta(days=TOKEN_EXPIRY_DAYS)
return {
'qr_url': qr_url,
'token_raw': token_raw, # 전체 토큰 (해시 생성용)
'token_hash': token_hash,
'claimable_points': claimable_points,
'expires_at': expires_at,
'pharmacy_id': pharmacy_id,
'transaction_id': transaction_id,
'total_amount': total_amount
}
def calculate_claimable_points(total_amount):
"""
적립 포인트 계산 (3% 정책)
Args:
total_amount (float): 판매 금액
Returns:
int: 적립 포인트 (정수, 소수점 절사)
"""
return int(total_amount * MILEAGE_RATE)
def save_token_to_db(transaction_id, token_hash, total_amount, claimable_points,
expires_at, pharmacy_id):
"""
생성된 토큰을 SQLite DB에 저장
Args:
transaction_id (str): 거래 ID
token_hash (str): SHA256 해시
total_amount (float): 판매 금액
claimable_points (int): 적립 포인트
expires_at (datetime): 만료일시
pharmacy_id (str): 약국 코드
Returns:
tuple: (성공 여부, 에러 메시지 or None)
중복 방지:
- transaction_id가 이미 존재하면 실패
- token_hash가 이미 존재하면 실패 (UNIQUE 제약)
"""
try:
db_manager = DatabaseManager()
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
# 중복 체크 (transaction_id)
cursor.execute("""
SELECT id FROM claim_tokens WHERE transaction_id = ?
""", (transaction_id,))
if cursor.fetchone():
return (False, f"이미 QR이 생성된 거래입니다: {transaction_id}")
# INSERT
cursor.execute("""
INSERT INTO claim_tokens (
transaction_id, pharmacy_id, token_hash,
total_amount, claimable_points, expires_at
) VALUES (?, ?, ?, ?, ?, ?)
""", (
transaction_id,
pharmacy_id,
token_hash,
int(total_amount), # INTEGER 타입
claimable_points,
expires_at.strftime('%Y-%m-%d %H:%M:%S')
))
conn.commit()
return (True, None)
except Exception as e:
return (False, f"DB 저장 실패: {str(e)}")
# 테스트 코드
if __name__ == "__main__":
# 테스트
test_tx_id = "20251024000042"
test_amount = 50000.0
print("=" * 80)
print("Claim Token 생성 테스트")
print("=" * 80)
token_info = generate_claim_token(test_tx_id, test_amount)
print(f"거래 ID: {test_tx_id}")
print(f"판매 금액: {test_amount:,}")
print(f"적립 포인트: {token_info['claimable_points']}P")
print(f"토큰 원문: {token_info['token_raw'][:80]}...")
print(f"토큰 해시: {token_info['token_hash']}")
print(f"QR URL: {token_info['qr_url'][:80]}...")
print(f"만료일: {token_info['expires_at']}")
print("=" * 80)
# DB 저장 테스트
print("\nDB 저장 테스트...")
success, error = save_token_to_db(
test_tx_id,
token_info['token_hash'],
test_amount,
token_info['claimable_points'],
token_info['expires_at'],
token_info['pharmacy_id']
)
if success:
print("[OK] DB 저장 성공")
print(f"\nSQLite DB 경로: backend/db/mileage.db")
print("다음 명령으로 확인:")
print(" sqlite3 backend/db/mileage.db \"SELECT * FROM claim_tokens\"")
else:
print(f"[ERROR] {error}")