feat: 포인트 사용 기능 및 시간 표시 개선

- UTC to KST 시간 변환 로직 추가 (SQLite 저장 시간 표시용)
- 관리자 페이지에 포인트 사용(차감) 기능 추가
  - 사용자 상세 모달에 "포인트 사용" 버튼 추가
  - 포인트 입력 및 차감 처리
  - 마일리지 원장에 USE 타입으로 기록
- 구매 이력 시간을 MSSQL의 실제 거래 시간(InsertTime)으로 수정
- 선택적 시간 변환 적용
  - 변환: users.created_at, mileage_ledger.created_at, claim_tokens.created_at
  - 미변환: claim_tokens.claimed_at, MSSQL 거래 시간
- 관리자 페이지에 검색 기능 추가 (사이드바)
  - 사용자 검색 (이름, 전화번호, 뒷자리)
  - 제품 검색 (약품명으로 구매자 조회)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 21:54:12 +09:00
parent 7627efbdfb
commit 59a33cc249
3 changed files with 328 additions and 20 deletions

View File

@@ -5,9 +5,10 @@ Flask 웹 서버 - QR 마일리지 적립
from flask import Flask, request, render_template, jsonify, redirect, url_for
import hashlib
from datetime import datetime
from datetime import datetime, timezone, timedelta
import sys
import os
import logging
from sqlalchemy import text
# Path setup
@@ -20,6 +21,47 @@ app.secret_key = 'pharmacy-qr-mileage-secret-key-2026'
# 데이터베이스 매니저
db_manager = DatabaseManager()
# KST 타임존 (UTC+9)
KST = timezone(timedelta(hours=9))
def utc_to_kst_str(utc_time_str):
"""
UTC 시간 문자열을 KST 시간 문자열로 변환
Args:
utc_time_str (str): UTC 시간 문자열 (ISO 8601 형식)
Returns:
str: KST 시간 문자열 (YYYY-MM-DD HH:MM:SS)
"""
if not utc_time_str:
return None
try:
# ISO 8601 형식 파싱 ('2026-01-23T12:28:36' 또는 '2026-01-23 12:28:36')
utc_time_str = utc_time_str.replace(' ', 'T') # 공백을 T로 변환
# datetime 객체로 변환
if 'T' in utc_time_str:
utc_time = datetime.fromisoformat(utc_time_str)
else:
utc_time = datetime.fromisoformat(utc_time_str)
# UTC 타임존 설정 (naive datetime인 경우)
if utc_time.tzinfo is None:
utc_time = utc_time.replace(tzinfo=timezone.utc)
# KST로 변환
kst_time = utc_time.astimezone(KST)
# 문자열로 반환 (초 단위까지만)
return kst_time.strftime('%Y-%m-%d %H:%M:%S')
except Exception as e:
logging.error(f"시간 변환 실패: {utc_time_str}, 오류: {e}")
return utc_time_str # 변환 실패 시 원본 반환
def verify_claim_token(transaction_id, nonce):
"""
@@ -355,11 +397,15 @@ def my_page():
FROM users WHERE phone = ?
""", (phone,))
user = cursor.fetchone()
user_raw = cursor.fetchone()
if not user:
if not user_raw:
return render_template('error.html', message='등록되지 않은 전화번호입니다.')
# 사용자 정보에 KST 시간 변환 적용
user = dict(user_raw)
user['created_at'] = utc_to_kst_str(user_raw['created_at'])
# 적립 내역 조회
cursor.execute("""
SELECT points, balance_after, reason, description, created_at
@@ -369,7 +415,14 @@ def my_page():
LIMIT 20
""", (user['id'],))
transactions = cursor.fetchall()
transactions_raw = cursor.fetchall()
# 거래 내역에 KST 시간 변환 적용
transactions = []
for tx in transactions_raw:
tx_dict = dict(tx)
tx_dict['created_at'] = utc_to_kst_str(tx['created_at'])
transactions.append(tx_dict)
return render_template('my_page.html', user=user, transactions=transactions)
@@ -506,6 +559,24 @@ def admin_user_detail(user_id):
for token in claimed_tokens:
transaction_id = token['transaction_id']
# SALE_MAIN에서 거래 시간 조회
sale_main_query = text("""
SELECT InsertTime
FROM SALE_MAIN
WHERE SL_NO_order = :transaction_id
""")
sale_main = session.execute(
sale_main_query,
{'transaction_id': transaction_id}
).fetchone()
# 거래 시간 추출 (MSSQL의 실제 거래 시간)
if sale_main and sale_main.InsertTime:
transaction_date = str(sale_main.InsertTime)[:16].replace('T', ' ')
else:
transaction_date = '-'
# SALE_SUB + CD_GOODS JOIN
sale_items_query = text("""
SELECT
@@ -551,7 +622,7 @@ def admin_user_detail(user_id):
purchases.append({
'transaction_id': transaction_id,
'date': str(token['claimed_at'])[:16].replace('T', ' '),
'date': transaction_date, # MSSQL의 실제 거래 시간 사용
'amount': int(token['total_amount']),
'points': int(token['claimable_points']),
'items_summary': items_summary,
@@ -572,7 +643,7 @@ def admin_user_detail(user_id):
'name': user['nickname'],
'phone': user['phone'],
'balance': user['mileage_balance'],
'created_at': str(user['created_at'])[:16].replace('T', ' ')
'created_at': utc_to_kst_str(user['created_at'])
},
'mileage_history': [
{
@@ -580,7 +651,7 @@ def admin_user_detail(user_id):
'balance_after': ml['balance_after'],
'reason': ml['reason'],
'description': ml['description'],
'created_at': str(ml['created_at'])[:16].replace('T', ' '),
'created_at': utc_to_kst_str(ml['created_at']),
'transaction_id': ml['transaction_id']
}
for ml in mileage_history
@@ -753,6 +824,93 @@ def admin_search_product():
}), 500
@app.route('/admin/use-points', methods=['POST'])
def admin_use_points():
"""관리자 페이지에서 포인트 사용 (차감)"""
try:
data = request.get_json()
user_id = data.get('user_id')
points_to_use = data.get('points')
if not user_id or not points_to_use:
return jsonify({
'success': False,
'message': '사용자 ID와 포인트를 입력하세요.'
}), 400
if points_to_use <= 0:
return jsonify({
'success': False,
'message': '1 이상의 포인트를 입력하세요.'
}), 400
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
# 1. 현재 사용자 잔액 확인
cursor.execute("""
SELECT id, nickname, mileage_balance
FROM users
WHERE id = ?
""", (user_id,))
user = cursor.fetchone()
if not user:
return jsonify({
'success': False,
'message': '사용자를 찾을 수 없습니다.'
}), 404
current_balance = user['mileage_balance']
# 2. 잔액 확인
if points_to_use > current_balance:
return jsonify({
'success': False,
'message': f'잔액({current_balance:,}P)이 부족합니다.'
}), 400
# 3. 포인트 차감
new_balance = current_balance - points_to_use
cursor.execute("""
UPDATE users
SET mileage_balance = ?
WHERE id = ?
""", (new_balance, user_id))
# 4. 마일리지 원장 기록
cursor.execute("""
INSERT INTO mileage_ledger
(user_id, transaction_id, points, balance_after, reason, description)
VALUES (?, NULL, ?, ?, 'USE', ?)
""", (
user_id,
-points_to_use, # 음수로 저장
new_balance,
f'포인트 사용 (관리자) - {points_to_use:,}P 차감'
))
conn.commit()
logging.info(f"포인트 사용 완료: 사용자 ID {user_id}, 차감 {points_to_use}P, 잔액 {new_balance}P")
return jsonify({
'success': True,
'message': f'{points_to_use:,}P 사용 완료',
'new_balance': new_balance,
'used_points': points_to_use
})
except Exception as e:
logging.error(f"포인트 사용 실패: {e}")
return jsonify({
'success': False,
'message': f'포인트 사용 실패: {str(e)}'
}), 500
@app.route('/admin')
def admin():
"""관리자 페이지 - 전체 사용자 및 적립 현황"""
@@ -775,7 +933,14 @@ def admin():
ORDER BY created_at DESC
LIMIT 20
""")
recent_users = cursor.fetchall()
recent_users_raw = cursor.fetchall()
# 시간을 KST로 변환
recent_users = []
for user in recent_users_raw:
user_dict = dict(user)
user_dict['created_at'] = utc_to_kst_str(user['created_at'])
recent_users.append(user_dict)
# 최근 적립 내역 (50건)
cursor.execute("""
@@ -793,7 +958,14 @@ def admin():
ORDER BY ml.created_at DESC
LIMIT 50
""")
recent_transactions = cursor.fetchall()
recent_transactions_raw = cursor.fetchall()
# 시간을 KST로 변환
recent_transactions = []
for trans in recent_transactions_raw:
trans_dict = dict(trans)
trans_dict['created_at'] = utc_to_kst_str(trans['created_at'])
recent_transactions.append(trans_dict)
# QR 토큰 통계
cursor.execute("""
@@ -820,14 +992,22 @@ def admin():
ORDER BY created_at DESC
LIMIT 20
""")
recent_tokens = cursor.fetchall()
recent_tokens_raw = cursor.fetchall()
# Convert only created_at (발행일), leave claimed_at (적립일) unconverted
recent_tokens = []
for token in recent_tokens_raw:
token_dict = dict(token)
token_dict['created_at'] = utc_to_kst_str(token['created_at']) # Convert 발행일
# claimed_at stays as-is (or remains None if not claimed)
recent_tokens.append(token_dict)
return render_template('admin.html',
stats=stats,
recent_users=recent_users,
recent_transactions=recent_transactions,
token_stats=token_stats,
recent_tokens=recent_tokens)
stats=stats,
recent_users=recent_users,
recent_transactions=recent_transactions,
token_stats=token_stats,
recent_tokens=recent_tokens)
if __name__ == '__main__':