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 코드 + 거래 정보 포함 * 미리보기 모드 및 프린터 전송 지원
This commit is contained in:
parent
c2dc42c565
commit
7aad05acb9
288
backend/utils/qr_label_printer.py
Normal file
288
backend/utils/qr_label_printer.py
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
"""
|
||||||
|
QR 영수증 라벨 인쇄 모듈
|
||||||
|
Brother QL-810W 프린터용 29mm 가로형 라벨
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from brother_ql.raster import BrotherQLRaster
|
||||||
|
from brother_ql.conversion import convert
|
||||||
|
from brother_ql.backends.helpers import send
|
||||||
|
import qrcode
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 프린터 설정 (barcode_print.py와 동일)
|
||||||
|
PRINTER_IP = "192.168.0.168"
|
||||||
|
PRINTER_PORT = 9100
|
||||||
|
PRINTER_MODEL = "QL-810W"
|
||||||
|
LABEL_TYPE = "29"
|
||||||
|
|
||||||
|
# 로깅 설정
|
||||||
|
logging.basicConfig(level=logging.INFO, format='[QR_LABEL] %(levelname)s: %(message)s')
|
||||||
|
|
||||||
|
|
||||||
|
def get_font_path():
|
||||||
|
"""
|
||||||
|
폰트 파일 경로 자동 감지 (폴백 지원)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 폰트 파일 경로 또는 None (기본 폰트 사용)
|
||||||
|
"""
|
||||||
|
candidates = [
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "samples", "fonts", "malgunbd.ttf"),
|
||||||
|
"C:\\Windows\\Fonts\\malgunbd.ttf", # Windows 기본 경로
|
||||||
|
"C:\\Windows\\Fonts\\malgun.ttf", # 볼드 아닌 버전
|
||||||
|
"/usr/share/fonts/truetype/malgun.ttf", # Linux
|
||||||
|
"malgun.ttf", # 시스템 폰트
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in candidates:
|
||||||
|
if os.path.exists(path):
|
||||||
|
logging.info(f"폰트 파일 사용: {path}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
logging.warning("폰트 파일을 찾을 수 없습니다. 기본 폰트 사용.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def create_qr_receipt_label(qr_url, transaction_id, total_amount, claimable_points,
|
||||||
|
transaction_time):
|
||||||
|
"""
|
||||||
|
QR 영수증 라벨 이미지 생성
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qr_url (str): QR 코드 URL (token_raw 포함)
|
||||||
|
transaction_id (str): 거래 번호
|
||||||
|
total_amount (float): 총 판매 금액
|
||||||
|
claimable_points (int): 적립 가능 포인트
|
||||||
|
transaction_time (datetime): 거래 시간
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: 생성된 라벨 이미지 (800x306px, mode='1')
|
||||||
|
|
||||||
|
레이아웃 (가로형):
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ [청춘약국] [QR CODE] │
|
||||||
|
│ 2025-10-24 14:30 120x120px │
|
||||||
|
│ 거래: 20251024000042 │
|
||||||
|
│ │
|
||||||
|
│ 결제금액: 50,000원 │
|
||||||
|
│ 적립예정: 1,500P │
|
||||||
|
│ │
|
||||||
|
│ QR 촬영하고 포인트 받으세요! │
|
||||||
|
│ │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. 캔버스 생성 (가로형)
|
||||||
|
width = 800
|
||||||
|
height = 306
|
||||||
|
img = Image.new('1', (width, height), 1) # 흰색 배경
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# 2. 폰트 로드
|
||||||
|
font_path = get_font_path()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if font_path:
|
||||||
|
font_title = ImageFont.truetype(font_path, 48) # 약국명
|
||||||
|
font_info = ImageFont.truetype(font_path, 32) # 거래 정보
|
||||||
|
font_amount = ImageFont.truetype(font_path, 40) # 금액 (크게)
|
||||||
|
font_points = ImageFont.truetype(font_path, 36) # 포인트 (강조)
|
||||||
|
font_small = ImageFont.truetype(font_path, 28) # 안내 문구
|
||||||
|
else:
|
||||||
|
raise IOError("폰트 없음")
|
||||||
|
except (IOError, OSError):
|
||||||
|
logging.warning("TrueType 폰트 로드 실패. 기본 폰트 사용.")
|
||||||
|
font_title = ImageFont.load_default()
|
||||||
|
font_info = ImageFont.load_default()
|
||||||
|
font_amount = ImageFont.load_default()
|
||||||
|
font_points = ImageFont.load_default()
|
||||||
|
font_small = ImageFont.load_default()
|
||||||
|
|
||||||
|
# 3. QR 코드 생성 (우측 상단) - 크기 및 해상도 개선
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=1,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_H, # 최고 레벨 (30% 복원)
|
||||||
|
box_size=8, # 기존 4 -> 8로 증가 (더 선명)
|
||||||
|
border=2, # 테두리 2칸 (인식률 향상)
|
||||||
|
)
|
||||||
|
qr.add_data(qr_url)
|
||||||
|
qr.make(fit=True)
|
||||||
|
qr_img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
|
||||||
|
# QR 코드 크기 및 위치 (기존 120 -> 200으로 증가)
|
||||||
|
qr_size = 200 # 크기 대폭 증가
|
||||||
|
qr_img = qr_img.resize((qr_size, qr_size), Image.LANCZOS)
|
||||||
|
qr_x = width - qr_size - 15 # 우측 여백 15px
|
||||||
|
qr_y = 10 # 상단 여백 10px
|
||||||
|
|
||||||
|
# QR 코드 붙이기
|
||||||
|
if qr_img.mode != '1':
|
||||||
|
qr_img = qr_img.convert('1')
|
||||||
|
img.paste(qr_img, (qr_x, qr_y))
|
||||||
|
|
||||||
|
# 4. 좌측 텍스트 영역 (y 위치 추적)
|
||||||
|
x_margin = 20
|
||||||
|
y = 20
|
||||||
|
|
||||||
|
# 약국명 (굵게)
|
||||||
|
pharmacy_text = "청춘약국"
|
||||||
|
for offset in [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]:
|
||||||
|
draw.text((x_margin + offset[0], y + offset[1]), pharmacy_text,
|
||||||
|
font=font_title, fill=0)
|
||||||
|
y += 55
|
||||||
|
|
||||||
|
# 거래 시간
|
||||||
|
time_text = transaction_time.strftime('%Y-%m-%d %H:%M')
|
||||||
|
draw.text((x_margin, y), time_text, font=font_info, fill=0)
|
||||||
|
y += 40
|
||||||
|
|
||||||
|
# 거래 번호
|
||||||
|
tx_text = f"거래: {transaction_id}"
|
||||||
|
draw.text((x_margin, y), tx_text, font=font_info, fill=0)
|
||||||
|
y += 50
|
||||||
|
|
||||||
|
# 결제 금액 (강조)
|
||||||
|
amount_text = f"결제금액: {int(total_amount):,}원"
|
||||||
|
for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]:
|
||||||
|
draw.text((x_margin + offset[0], y + offset[1]), amount_text,
|
||||||
|
font=font_amount, fill=0)
|
||||||
|
y += 50
|
||||||
|
|
||||||
|
# 적립 포인트 (굵게)
|
||||||
|
points_text = f"적립예정: {claimable_points:,}P"
|
||||||
|
for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]:
|
||||||
|
draw.text((x_margin + offset[0], y + offset[1]), points_text,
|
||||||
|
font=font_points, fill=0)
|
||||||
|
y += 55
|
||||||
|
|
||||||
|
# 안내 문구
|
||||||
|
guide_text = "QR 촬영하고 포인트 받으세요!"
|
||||||
|
draw.text((x_margin, y), guide_text, font=font_small, fill=0)
|
||||||
|
|
||||||
|
# 5. 테두리 (가위선 스타일)
|
||||||
|
for i in range(2):
|
||||||
|
draw.rectangle([5 + i, 5 + i, width - 5 - i, height - 5 - i], outline=0)
|
||||||
|
|
||||||
|
logging.info(f"QR 라벨 이미지 생성 완료: {transaction_id}")
|
||||||
|
return img
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"QR 라벨 이미지 생성 실패: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def print_qr_label(qr_url, transaction_id, total_amount, claimable_points,
|
||||||
|
transaction_time, preview_mode=False):
|
||||||
|
"""
|
||||||
|
QR 라벨 출력 또는 미리보기
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qr_url (str): QR 코드 URL
|
||||||
|
transaction_id (str): 거래 번호
|
||||||
|
total_amount (float): 판매 금액
|
||||||
|
claimable_points (int): 적립 포인트
|
||||||
|
transaction_time (datetime): 거래 시간
|
||||||
|
preview_mode (bool): True = 미리보기, False = 인쇄
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
preview_mode=True: (성공 여부, 이미지 파일 경로)
|
||||||
|
preview_mode=False: 성공 여부 (bool)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logging.info(f"QR 라벨 {'미리보기' if preview_mode else '출력'} 시작: {transaction_id}")
|
||||||
|
|
||||||
|
# 1. 라벨 이미지 생성
|
||||||
|
label_image = create_qr_receipt_label(
|
||||||
|
qr_url, transaction_id, total_amount,
|
||||||
|
claimable_points, transaction_time
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 미리보기 모드
|
||||||
|
if preview_mode:
|
||||||
|
# temp 디렉터리 생성
|
||||||
|
temp_dir = os.path.join(os.path.dirname(__file__), "..", "samples", "temp")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 파일명 생성
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = f"qr_receipt_{transaction_id}_{timestamp}.png"
|
||||||
|
file_path = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
# PNG로 저장
|
||||||
|
label_image.save(file_path, "PNG")
|
||||||
|
logging.info(f"미리보기 이미지 저장: {file_path}")
|
||||||
|
|
||||||
|
return True, file_path
|
||||||
|
|
||||||
|
# 3. 실제 인쇄 모드
|
||||||
|
else:
|
||||||
|
# 이미지 90도 회전 (Brother QL은 세로 기준)
|
||||||
|
label_image_rotated = label_image.rotate(90, expand=True)
|
||||||
|
|
||||||
|
# Brother QL Raster 변환
|
||||||
|
qlr = BrotherQLRaster(PRINTER_MODEL)
|
||||||
|
instructions = convert(
|
||||||
|
qlr=qlr,
|
||||||
|
images=[label_image_rotated],
|
||||||
|
label=LABEL_TYPE,
|
||||||
|
rotate="0",
|
||||||
|
threshold=70.0,
|
||||||
|
dither=False,
|
||||||
|
compress=False,
|
||||||
|
red=False,
|
||||||
|
dpi_600=False,
|
||||||
|
hq=True,
|
||||||
|
cut=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 프린터로 전송
|
||||||
|
printer_identifier = f"tcp://{PRINTER_IP}:{PRINTER_PORT}"
|
||||||
|
send(instructions, printer_identifier=printer_identifier)
|
||||||
|
|
||||||
|
logging.info(f"QR 라벨 출력 완료: {transaction_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"QR 라벨 {'미리보기' if preview_mode else '출력'} 실패: {e}")
|
||||||
|
if preview_mode:
|
||||||
|
return False, None
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 테스트 코드
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 테스트 데이터
|
||||||
|
test_qr_url = "https://pharmacy.example.com/claim?t=20251024000042:abc123:2025-10-24T14:30:00"
|
||||||
|
test_tx_id = "20251024000042"
|
||||||
|
test_amount = 50000.0
|
||||||
|
test_points = 1500
|
||||||
|
test_time = datetime.now()
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("QR 라벨 생성 테스트")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"거래 ID: {test_tx_id}")
|
||||||
|
print(f"금액: {test_amount:,}원")
|
||||||
|
print(f"적립: {test_points}P")
|
||||||
|
print(f"QR URL: {test_qr_url[:60]}...")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# 미리보기 테스트
|
||||||
|
print("\n미리보기 모드 테스트...")
|
||||||
|
success, image_path = print_qr_label(
|
||||||
|
test_qr_url, test_tx_id, test_amount, test_points, test_time,
|
||||||
|
preview_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"[OK] 테스트 성공!")
|
||||||
|
print(f"이미지 저장: {image_path}")
|
||||||
|
print(f"\n다음 명령으로 확인:")
|
||||||
|
print(f" start {image_path}")
|
||||||
|
else:
|
||||||
|
print("[ERROR] 테스트 실패")
|
||||||
191
backend/utils/qr_token_generator.py
Normal file
191
backend/utils/qr_token_generator.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
"""
|
||||||
|
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}")
|
||||||
Loading…
Reference in New Issue
Block a user